diff --git a/bom/pom.xml b/bom/pom.xml index ec6f9ac9a3a..d4b33c87815 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -182,6 +182,17 @@ helidon-microprofile-cors ${helidon.version} + + + io.helidon.graphql + helidon-graphql-server + ${helidon.version} + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + ${helidon.version} + io.helidon.integrations.micronaut diff --git a/common/common/src/main/java/io/helidon/common/FeatureCatalog.java b/common/common/src/main/java/io/helidon/common/FeatureCatalog.java index 296bf5f34e5..c7c22613708 100644 --- a/common/common/src/main/java/io/helidon/common/FeatureCatalog.java +++ b/common/common/src/main/java/io/helidon/common/FeatureCatalog.java @@ -152,6 +152,14 @@ final class FeatureCatalog { .path("WebServer", "Websocket") .nativeSupported(true) .nativeDescription("Server only")); + add("io.helidon.graphql.server", + FeatureDescriptor.builder() + .name("GraphQL") + .description("GraphQL support") + .path("GraphQL") + .nativeDescription("Experimental support, tested on limited use cases") + .flavor(HelidonFlavor.SE) + .experimental(true)); /* * MP Modules @@ -203,6 +211,14 @@ final class FeatureCatalog { "Fault Tolerance", "MicroProfile Fault Tolerance spec implementation", "FT"); + add("io.helidon.microprofile.graphql.server", + FeatureDescriptor.builder() + .name("GraphQL") + .description("MicroProfile GraphQL spec implementation") + .path("GraphQL") + .nativeDescription("Experimental support, tested on limited use cases") + .flavor(HelidonFlavor.MP) + .experimental(true)); add("io.helidon.microprofile.grpc.server", FeatureDescriptor.builder() .name("gRPC Server") diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 1fbf015f9db..bc22fd0caed 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -53,6 +53,8 @@ 1.30.11 2.3.3 20.2.0 + 15.0 + 15.0.0 1.32.1 28.1-jre 1.4.199 @@ -91,6 +93,7 @@ 3.3.1 1.4 2.1.1 + 1.0.3 2.2 1.1.1 2.3.2 @@ -588,6 +591,22 @@ + + org.eclipse.microprofile.graphql + microprofile-graphql-api + ${version.lib.microprofile-graphql} + + + org.osgi + org.osgi.annotation.versioning + + + + + org.eclipse.microprofile.graphql + microprofile-graphql-tck + ${version.lib.microprofile-graphql} + org.eclipse.microprofile.jwt microprofile-jwt-auth-api @@ -976,6 +995,26 @@ graal-sdk ${version.lib.graalvm} + + com.graphql-java + graphql-java + ${version.lib.graphql-java} + + + com.graphql-java + graphql-java-extended-scalars + ${version.lib.graphql-java.extended.scalars} + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okio + okio + + + org.graalvm.nativeimage svm diff --git a/docs/mp/graphql/01_mp_graphql.adoc b/docs/mp/graphql/01_mp_graphql.adoc new file mode 100644 index 00000000000..7c064b3d6a6 --- /dev/null +++ b/docs/mp/graphql/01_mp_graphql.adoc @@ -0,0 +1,221 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019, 2020 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += MicroProfile GraphQL +:h1Prefix: MP +:pagename: microprofile-graphql +:description: Helidon GraphQL MicroProfile +:keywords: helidon, graphql, microprofile, micro-profile + + +The Microprofile GraphQL APIs are an extension to <> +to allow building of applications that can expose a GraphQL endpoint. + +== Experimental + +WARNING: The Helidon GraphQL feature is currently experimental and the APIs are + subject to changes until GraphQL support is stabilized. + +== About the MicroProfile GraphQL Specification +Helidon MP implements the MicroProfile GraphQL +link:https://github.com/eclipse/microprofile-graphql[spec] version {micprofile-graphql-version}. +The spec prescribes how applications can be built to expose an endpoint for GraphQL. +GraphQL is an open-source data query and manipulation language for APIs, +and a runtime for fulfilling queries with existing data. +It provides an alternative to, though not necessarily a replacement for, REST. + +For more information on GraphQL see https://graphql.org/. + +== Maven Coordinates + +The <> page describes how you +should declare dependency management for Helidon applications. Then declare the following dependency in your project: + +[source,xml] +---- + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + +---- + +== Getting Started + +=== Defining your API + +The MicroProfile GraphQL specification defines a number of key annotations to be used when writing a GraphQL endpoint: + +* `@GraphQLApi` - identifies a CDI Bean as a GraphQL Endpoint +* `@Query` - identifies a method as returning specified fields for an object or collection of entities +* `@Mutation` - identifies a method which creates, deletes or updates entities + +NOTE: Please see the link:https://github.com/eclipse/microprofile-graphql[Microprofile GraphQL spec] for the full list of available annotations. + +For example, the following defines a GraphQL endpoint with a number of queries and mutations that work +against a fictional `CustomerService` service and `Customer` class. + +[source,java] +.Simple ContactGraphQLApi +---- +@ApplicationScoped +@org.eclipse.microprofile.graphql.GraphQLApi +public class ContactGraphQLApi { + + @Inject + private CustomerService customerService; + + @org.eclipse.microprofile.graphql.Query + public Collection findAllCustomers() { <1> + return customerService.getAllCustomers(); + } + + @org.eclipse.microprofile.graphql.Query + public Customer findCustomer(@Name("customerId") int id) { <2> + return customerService.getCustomer(id); + } + + @org.eclipse.microprofile.graphql.Query + public Collection findCustomersByName(@Name("name") String name) { <3> + return customerService.getAllCustomers(name); + } + + @org.eclipse.microprofile.graphql.Mutation + public Contact createCustomer(@Name("customerId") int id, <4> + @Name("name") String name, + @Name("balance") float balance) { + return customerService.createCustomer(id, name, balance); + } +} + +public class customer { + private int id; + @NonNull + private String name; + private float balance; + + // getters and setters omitted for brevity +} +---- + +<1> a query with no-arguments that will return all Customers +<2> a query that takes an argument to return a specific Customer +<3> a query that optionally takes a name and returns a collection of Customers +<4> a mutation that creates a Customer and returns the newly created Customer + +The above would generate a GraphQL schema as shown below: +[source,graphql] +.Sample GraphQL Schema +---- +type Query { + findAllCustomers: [Customer] + findCustomer(customerId: Int!): Customer + findCustomersByName(name: String): [Customers] +} + +type Mutation { + createCustomer(customerId: Int!, name: String!, balance: Float!): Customer +} + +type Customer { + id: Int! + name: String! + balance: Float +} +---- + +After application startup, a GraphQL schema will be generated from your annotated API classes +and POJO's and you will be able to access these via the URL's described below. + +=== Creating your entry-point + +As per the instructions <> ensure you have added a +`src/main/resources/META-INF/beans.xml` file, so the CDI implementation can pick up your classes. + +A `Main` class is not needed, you can configure `io.helidon.microprofile.cdi.Main` as the entry point. + +Optionally, you can configure a custom entry point (such as when you need custom configuration setup). + +[source,java] +.Sample Entry-point +---- +public class MyMain { + public static void main(String[] args) { + io.helidon.microprofile.cdi.Main.main(args); + } +} +---- + +=== Building your application + +As part of building your application, you must create a Jandex index +using the `jandex-maven-plugin` for all API and POJO classes that are used. + +[source,xml] +.Generate Jandex index +---- + +org.jboss.jandex +jandex-maven-plugin + + + make-index + + + +---- + +== Accessing the GraphQL end-points + +After starting your application you should see a message similar to the following indicating the GraphQL support is available: + +[source,bash] +.Sample Startup output +---- +2020.11.16 12:29:58 INFO io.helidon.common.HelidonFeatures Thread[features-thread,5,main]: Helidon MP 2.1.1-SNAPSHOT features: [CDI, Config, Fault Tolerance, GraphQL, Health, JAX-RS, Metrics, Open API, Security, Server, Tracing] +2020.11.16 12:29:58 INFO io.helidon.common.HelidonFeatures.experimental Thread[features-thread,5,main]: You are using experimental features. These APIs may change, please follow changelog! +2020.11.16 12:29:58 INFO io.helidon.common.HelidonFeatures.experimental Thread[features-thread,5,main]: Experimental feature: GraphQL (GraphQL) +---- + +You can then use your GraphQL client via the default endpoint `http://host:port/graphql`. + +The GraphQL Schema is available via `http://host:port/graphql/schema.graphql`. + +NOTE: If you wish to use the GraphiQL UI (https://github.com/graphql/graphiql) then please see the Helidon Microprofile GraphQL +example at the following URL: +https://github.com/oracle/helidon/tree/master/examples/microprofile/graphql + +== Configuration Options + +=== MicroProfile GraphQL +The specification defines the following configuration options: + +[cols="2,2,5"] + +|=== +|key |default value |description + +|`mp.graphql.defaultErrorMessage` |`Server Error` |Error message to send to caller in case of error +|`mp.graphql.exceptionsBlackList` |{nbsp} |Array of checked exception classes that should return default error message +|`mp.graphql.exceptionsWhiteList` |{nbsp} |Array of unchecked exception classes that should return message to caller (instead of default error message) + +|=== + +These configuration options are more significant that the configuration options + that can be used to configure GraphQL invocation (see below). + +include::../../shared/graphql/configuration.adoc[] diff --git a/docs/mp/introduction/01_introduction.adoc b/docs/mp/introduction/01_introduction.adoc index 1be4abae08b..696e0daec17 100644 --- a/docs/mp/introduction/01_introduction.adoc +++ b/docs/mp/introduction/01_introduction.adoc @@ -85,6 +85,14 @@ Add support for CORS to your application using a Helidon module. Defines annotations that improve applications by providing support to handle error conditions (faults). -- +//GraphQL +[CARD] +.GraphQL +[icon=graphic_eq,link=mp/graphql/01_mp_graphql.adoc] +-- +Expose GraphQL API using Microprofile GraphQL. +-- + //gRPC [CARD] .gRPC @@ -92,6 +100,7 @@ Defines annotations that improve applications by providing support to handle err -- Build gRPC servers and clients. -- + //Health Checks [CARD] .Health Checks diff --git a/docs/se/graphql/01_introduction.adoc b/docs/se/graphql/01_introduction.adoc new file mode 100644 index 00000000000..c0dead85390 --- /dev/null +++ b/docs/se/graphql/01_introduction.adoc @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019, 2020 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += GraphQL Server Introduction +:h1Prefix: SE +:pagename: graphql-server-introduction +:description: Helidon GraphQL Server Introduction +:keywords: helidon, graphql, java + +Helidon GraphQL Server provides a framework for creating link:https://github.com/graphql-java/graphql-java[GraphQL] applications. + +== Experimental + +WARNING: The Helidon GraphQL feature is currently experimental and the APIs are + subject to changes until GraphQL support is stabilized. + +== Quick Start + +Here is the code for a minimalist GraphQL application that exposes 2 queries. + +[source,java] +---- + public static void main(String[] args) { + WebServer server = WebServer.builder() + .routing(Routing.builder() + .register(GraphQlSupport.create(buildSchema())) <1> + .build()) + .build(); + + server.start() <2> + .thenApply(webServer -> { + String endpoint = "http://localhost:" + webServer.port(); + System.out.println("GraphQL started on " + endpoint + "/graphql"); + System.out.println("GraphQL schema availanle on " + endpoint + "/graphql/schema.graphql"); + return null; + }); + } + + private static GraphQLSchema buildSchema() { + String schema = "type Query{\n" <3> + + "hello: String \n" + + "helloInDifferentLanguages: [String] \n" + + "\n}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + // DataFetcher to return various hello's in difference languages <4> + DataFetcher> hellosDataFetcher = (DataFetcher>) environment -> + List.of("Bonjour", "Hola", "Zdravstvuyte", "Nǐn hǎo", "Salve", "Gudday", "Konnichiwa", "Guten Tag"); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() <5> + .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world"))) + .type("Query", builder -> builder.dataFetcher("helloInDifferentLanguages", hellosDataFetcher)) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); <6> + } +---- + +<1> Register GraphQL support. +<2> Start the server. +<3> Define the GraphQL schema. +<4> Create a DataFetcher to return a List of Hellos in different languages. +<5> Wire up the DataFetchers. +<6> Generate the GraphQL schema. + +The example above deploys a very simple service exposing the `/graphql` endpoint. + +You can then probe the endpoints: + +1. Hello word endpoint ++ +[source,bash] +---- +curl -X POST http://127.0.0.1:PORT/graphql -d '{"query":"query { hello }"}' + +"data":{"hello":"world"}} +---- + +2. Hello in different languages ++ +[source,bash] +---- +curl -X POST http://127.0.0.1:PORT/graphql -d '{"query":"query { helloInDifferentLanguages }"}' + +{"data":{"helloInDifferentLanguages":["Bonjour","Hola","Zdravstvuyte","Nǐn hǎo","Salve","Gudday","Konnichiwa","Guten Tag"]}} +---- + +== Maven Coordinates + +The <> page describes how you +should declare dependency management for Helidon applications. Then declare the following dependency in your project: + +[source,xml] +---- + + io.helidon.graphql + helidon-graphql-server + +---- diff --git a/docs/se/introduction/01_introduction.adoc b/docs/se/introduction/01_introduction.adoc index 5eaab08a5b8..62d9bcc33d3 100644 --- a/docs/se/introduction/01_introduction.adoc +++ b/docs/se/introduction/01_introduction.adoc @@ -69,7 +69,13 @@ Add support for CORS to your application using a Helidon module. -- Provides a unified, reactive API for working with databases in non-blocking way. -- - +//GraphQL +[CARD] +.GraphQL +[icon=graphic_eq,link=se/graphql/01_introduction.adoc] +-- +Build GraphQL servers. +-- //gRPC [CARD] .gRPC diff --git a/docs/shared/graphql/configuration.adoc b/docs/shared/graphql/configuration.adoc new file mode 100644 index 00000000000..340b3ed85e7 --- /dev/null +++ b/docs/shared/graphql/configuration.adoc @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2020 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +=== Helidon GraphQL +In addition, we provide the following configuration options: + + +The following configuration keys can be used to set up integration with WebServer: + +[cols="2,2,5"] + +|=== +|key |default value |description + +|`graphql.web-context` |`/graphql` |Context that serves the GraphQL endpoint. +|`graphql.schema-uri` |`/schema.graphql` |URI that serves the schema (under web context) +|`graphql.cors` |{nbsp} |CORS configuration for this service +|`graphql.executor-service` |{nbsp} |Configuration of `ServerThreadPoolSupplier` used to set up executor service + +|=== + +The following configuration keys can be used to set up GraphQL invocation: + +[cols="2,2,5"] + +|=== +|key |default value |description + +|`graphql.default-error-message` |`Server Error` |Error message to send to caller in case of error +|`graphql.exception-white-list` |{nbsp} |Array of checked exception classes that should return default error message +|`graphql.exception-black-list` |{nbsp} |Array of unchecked exception classes that should return message to caller (instead of default error message) + +|=== \ No newline at end of file diff --git a/docs/sitegen.yaml b/docs/sitegen.yaml index 4f76984c30b..22acab488c1 100644 --- a/docs/sitegen.yaml +++ b/docs/sitegen.yaml @@ -24,6 +24,7 @@ engine: javadoc-base-url: "./apidocs/" helidon-version: "${project.version}" microprofile-openapi-version: "${version.lib.microprofile-openapi-api}" + micprofile-graphql-version: "${version.lib.microprofile-graphql}" jandex-plugin-version: ${version.plugin.jandex} mp-version: "3.3" guides-dir: "${project.basedir}/../examples/guides" @@ -120,6 +121,14 @@ backend: items: - includes: - "se/grpc/*.adoc" + - title: "GraphQL server" + pathprefix: "/se/graphql" + glyph: + type: "icon" + value: "graphic_eq" + items: + - includes: + - "se/graphql/*.adoc" - title: "Health Checks" pathprefix: "/se/health" glyph: @@ -277,6 +286,14 @@ backend: items: - includes: - "mp/grpc/*.adoc" + - title: "GraphQL" + pathprefix: "/mp/graphql" + glyph: + type: "icon" + value: "graphic_eq" + items: + - includes: + - "mp/graphql/*.adoc" - title: "Health Checks" pathprefix: "/mp/health" glyph: diff --git a/examples/graphql/basics/README.md b/examples/graphql/basics/README.md new file mode 100644 index 00000000000..3e39e8d945b --- /dev/null +++ b/examples/graphql/basics/README.md @@ -0,0 +1,33 @@ +# Helidon GraphQL Basic Example + +This example shows the basics of using Helidon SE GraphQL. The example +manually creates a GraphQL Schema using the [GraphQL Java](https://github.com/graphql-java/graphql-java) API. + +## Build and run + +Start the application: + +```bash +mvn package +java -jar target/helidon-examples-graphql-basics.jar +``` + +Note the port number reported by the application. + +Probe the GraphQL endpoints: + +1. Hello word endpoint: + + ```bash + curl -X POST http://127.0.0.1:PORT/graphql -d '{"query":"query { hello }"}' + + "data":{"hello":"world"}} + ``` + +1. Hello in different languages + + ```bash + curl -X POST http://127.0.0.1:PORT/graphql -d '{"query":"query { helloInDifferentLanguages }"}' + + {"data":{"helloInDifferentLanguages":["Bonjour","Hola","Zdravstvuyte","Nǐn hǎo","Salve","Gudday","Konnichiwa","Guten Tag"]}} + ``` diff --git a/examples/graphql/basics/pom.xml b/examples/graphql/basics/pom.xml new file mode 100644 index 00000000000..268c57b23af --- /dev/null +++ b/examples/graphql/basics/pom.xml @@ -0,0 +1,69 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 2.1.1-SNAPSHOT + ../../../applications/se/pom.xml + + io.helidon.examples.graphql + helidon-examples-graphql-basics + Helidon GraphQL Examples Basics + + + Basic usage of GraphQL in helidon SE + + + + io.helidon.examples.graphql.basics.Main + + + + + io.helidon.graphql + helidon-graphql-server + + + io.helidon.webserver + helidon-webserver + + + io.helidon.common + helidon-common + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java new file mode 100644 index 00000000000..22e63ad3ea6 --- /dev/null +++ b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.List; + +import io.helidon.graphql.server.GraphQlSupport; +import io.helidon.webserver.Routing; +import io.helidon.webserver.WebServer; + +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; + +/** + * Main class of Graphql SE integration example. + */ +public class Main { + + private Main() { + } + + /** + * Start the example. Prints endpoints to standard output. + * + * @param args not used + */ + public static void main(String[] args) { + WebServer server = WebServer.builder() + .routing(Routing.builder() + .register(GraphQlSupport.create(buildSchema())) + .build()) + .build(); + + server.start() + .thenApply(webServer -> { + String endpoint = "http://localhost:" + webServer.port(); + System.out.println("GraphQL started on " + endpoint + "/graphql"); + System.out.println("GraphQL schema available on " + endpoint + "/graphql/schema.graphql"); + return null; + }); + } + + /** + * Generate a {@link GraphQLSchema}. + * @return a {@link GraphQLSchema} + */ + private static GraphQLSchema buildSchema() { + String schema = "type Query{\n" + + "hello: String \n" + + "helloInDifferentLanguages: [String] \n" + + "\n}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + // DataFetcher to return various hello's in difference languages + DataFetcher> hellosDataFetcher = (DataFetcher>) environment -> + List.of("Bonjour", "Hola", "Zdravstvuyte", "Nǐn hǎo", "Salve", "Gudday", "Konnichiwa", "Guten Tag"); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world"))) + .type("Query", builder -> builder.dataFetcher("helloInDifferentLanguages", hellosDataFetcher)) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } +} diff --git a/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java new file mode 100644 index 00000000000..c18e3b4bfee --- /dev/null +++ b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Example of healthchecks in helidon SE. + */ +package io.helidon.examples.graphql.basics; diff --git a/examples/graphql/pom.xml b/examples/graphql/pom.xml new file mode 100644 index 00000000000..37266e383bc --- /dev/null +++ b/examples/graphql/pom.xml @@ -0,0 +1,35 @@ + + + + + 4.0.0 + + helidon-examples-project + io.helidon.examples + 2.1.1-SNAPSHOT + + io.helidon.examples.graphql + helidon-examples-graphql-project + pom + Helidon GraphQL Examples + + + basics + + diff --git a/examples/microprofile/graphql/README.md b/examples/microprofile/graphql/README.md new file mode 100644 index 00000000000..2435e82051e --- /dev/null +++ b/examples/microprofile/graphql/README.md @@ -0,0 +1,189 @@ +# Microprofile GraphQL Example + +This example creates a simple Task API using Helidon's implementation of the Microprofile GraphQL API Specification. + +See [here](https://github.com/eclipse/microprofile-graphql) for more information on the +Microprofile GraphQL Specification as well as the [Helidon documentation](https://helidon.io/docs/v2/#/mp/introduction/01_introduction) +for an introduction to using GraphQL in Helidon MP. + +## Running the example + +1. Build + +```bash +mvn clean install +``` + +2. Run the example + +```bash +java -jar target/helidon-examples-microprofile-graphql.jar +``` + +## Issuing GraphQL requests via REST + +Access the `/graphql` endpoint via `http://127.0.0.1:7001/graphql`: + +1. Display the generated GraphQL Schema + + ```bash + curl http://127.0.0.1:7001/graphql/schema.graphql + ``` + + This will produce the following: + + ```graphql + type Mutation { + "Create a task with the given description" + createTask(description: String): Task + "Remove all completed tasks and return the tasks left" + deleteCompletedTasks: [Task] + "Delete a task and return the deleted task details" + deleteTask(id: String): Task + "Update a task" + updateTask(completed: Boolean, description: String, id: String): Task + } + + type Query { + "Return a given task" + findTask(id: String): Task + "Query tasks and optionally specified only completed" + tasks(completed: Boolean): [Task] + } + + type Task { + completed: Boolean! + createdAt: BigInteger! + description: String + id: String + } + + "Custom: Built-in java.math.BigInteger" + scalar BigInteger + ``` + +1. Create a Task + + ```bash + curl -X POST http://127.0.0.1:7001/graphql -d '{"query":"mutation createTask { createTask(description: \"Task Description 1\") { id description createdAt completed }}"}' + ``` + + Response is a newly created task: + + ```json + {"data":{"createTask":{"id":"0d4a8d","description":"Task Description 1","createdAt":1605501774877,"completed":false}} + ``` + +## Incorporating the GraphiQL UI + +The [GraphiQL UI](https://github.com/graphql/graphiql), which provides a UI to execute GraphQL commands, is not included by default in Helidon's Microprofile GraphQL +implementation. You can follow the guide below to incorporate the UI into this example: + +1. Copy the contents in the sample index.html file from [here](https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md) +into the file at `examples/microprofile/graphql/src/main/resources/web/index.html` + +1. Change the URL in the line `fetch('https://my/graphql', {` to `http://127.0.0.1:7001/graphql` + +1. Build and run the example using the instructions above. + +1. Access the GraphiQL UI via the following URL: http://127.0.0.1:7001/ui. + +2. Copy the following commands into the editor on the left. + + ```graphql + # Fragment to allow shorcut to display all fields for a task + fragment task on Task { + id + description + createdAt + completed + } + + # Create a task + mutation createTask { + createTask(description: "Task Description 1") { + ...task + } + } + + # Create a task with empty description - will return error message + # Normally unchecked exceptions will not be displayed but + # We have overriden this in the microprofile-config.properties + mutation createTaskWithoutDescription { + createTask { + ...task + } + } + + # Find all the tasks + query findAllTasks { + tasks { + ...task + } + } + + # Find a task + query findTask { + findTask(id: "251474") { + ...task + } + } + + # Find completed Tasks + query findCompletedTasks { + tasks(completed: true) { + ...task + } + } + + # Find outstanding Tasks + query findOutstandingTasks { + tasks(completed: false) { + ...task + } + } + + mutation updateTask { + updateTask(id: "251474" description:"New Description") { + ...task + } + } + + mutation completeTask { + updateTask(id: "251474" completed:true) { + ...task + } + } + + # Delete a task + mutation deleteTask { + deleteTask(id: "1f6ae5") { + ...task + } + } + + # Delete completed + mutation deleteCompleted { + deleteCompletedTasks { + ...task + } + } + ``` + +3. Run individual commands by clicking on the `Play` button and choosing the query or mutation to run. + +4. Sample requests + + 1. Execute `createTask` + 2. Change the description and execute `createTask` + 3. Execute `findTask` to show the exception when a task does not exist + 3. Change the id and execute `findTask` to show your newly created task + 5. Execute `findAllTasks` to show the 2 tasks + 6. Change the id and execute `updateTask` to update the existing task + 7. Change the id and execute `completeTask` + 8. Execute `findAllTasks` to show the task completed + 9. Execute `findCompletedTasks` to show only completed tasks + 10. Execute `deleteCompleted` to delete completed task + 11. Execute `findCompletedTasks` to show no completed tasks + + \ No newline at end of file diff --git a/examples/microprofile/graphql/pom.xml b/examples/microprofile/graphql/pom.xml new file mode 100644 index 00000000000..1ad50c4bcfa --- /dev/null +++ b/examples/microprofile/graphql/pom.xml @@ -0,0 +1,68 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 2.1.1-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.examples.microprofile + helidon-examples-microprofile-graphql + Helidon Microprofile Examples GraphQL + + + Usage of GraphQL in Helidon MP + + + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + + + io.helidon.microprofile.bundles + helidon-microprofile + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java new file mode 100644 index 00000000000..1ed8d580f1e --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.UUID; + +import io.helidon.common.Reflected; + +import org.eclipse.microprofile.graphql.NonNull; + +/** + * A data class representing a single To Do List task. + */ +@Reflected +public class Task { + + /** + * The creation time. + */ + private long createdAt; + + /** + * The completion status. + */ + private boolean completed; + + /** + * The task ID. + */ + @NonNull + private String id; + + /** + * The task description. + */ + @NonNull + private String description; + + /** + * Deserialization constructor. + */ + public Task() { + } + + /** + * Construct Task instance. + * + * @param description task description + */ + public Task(String description) { + this.id = UUID.randomUUID().toString().substring(0, 6); + this.createdAt = System.currentTimeMillis(); + this.description = description; + this.completed = false; + } + + /** + * Get the creation time. + * + * @return the creation time + */ + public long getCreatedAt() { + return createdAt; + } + + /** + * Get the task ID. + * + * @return the task ID + */ + public String getId() { + return id; + } + + /** + * Get the task description. + * + * @return the task description + */ + public String getDescription() { + return description; + } + + /** + * Set the task description. + * + * @param description the task description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get the completion status. + * + * @return true if it is completed, false otherwise. + */ + public boolean isCompleted() { + return completed; + } + + /** + * Sets the completion status. + * + * @param completed the completion status + */ + public void setCompleted(boolean completed) { + this.completed = completed; + } + + @Override + public String toString() { + return "Task{" + + "id=" + id + + ", description=" + description + + ", completed=" + completed + + '}'; + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java new file mode 100644 index 00000000000..17ef8f4812c --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * A CDI Bean that exposes a GraphQL API to query and mutate {@link Task}s. + */ +@GraphQLApi +@ApplicationScoped +public class TaskApi { + + private static final String MESSAGE = "Unable to find task with id "; + + private Map tasks = new ConcurrentHashMap<>(); + + /** + * Create a {@link Task}. + * + * @param description task description + * @return the created {@link Task} + */ + @Mutation + @Description("Create a task with the given description") + public Task createTask(@Name("description") String description) { + if (description == null) { + throw new IllegalArgumentException("Description must be provided"); + } + Task task = new Task(description); + tasks.put(task.getId(), task); + return task; + } + + /** + * Query {@link Task}s. + * + * @param completed optionally specify completion status + * @return a {@link Collection} of {@link Task}s + */ + @Query + @Description("Query tasks and optionally specify only completed") + public Collection getTasks(@Name("completed") Boolean completed) { + return tasks.values().stream() + .filter(task -> completed == null || task.isCompleted() == completed) + .collect(Collectors.toList()); + } + + /** + * Return a {@link Task}. + * + * @param id task id + * @return the {@link Task} with the given id + * @throws TaskNotFoundException if the task was not found + */ + @Query + @Description("Return a given task") + public Task findTask(@Name("id") String id) throws TaskNotFoundException { + return Optional.ofNullable(tasks.get(id)) + .orElseThrow(() -> new TaskNotFoundException(MESSAGE + id)); + } + + /** + * Delete a {@link Task}. + * + * @param id task to delete + * @return the deleted {@link Task} + * @throws TaskNotFoundException if the task was not found + */ + @Mutation + @Description("Delete a task and return the deleted task details") + public Task deleteTask(@Name("id") String id) throws TaskNotFoundException { + return Optional.ofNullable(tasks.remove(id)) + .orElseThrow(() -> new TaskNotFoundException(MESSAGE + id)); + } + + /** + * Remove all completed {@link Task}s. + * + * @return the {@link Task}s left + */ + @Mutation + @Description("Remove all completed tasks and return the tasks left") + public Collection deleteCompletedTasks() { + tasks.values().removeIf(Task::isCompleted); + return tasks.values(); + } + + /** + * Update a {@link Task}. + * + * @param id task to update + * @param description optional description + * @param completed optional completed + * @return the updated {@link Task} + * @throws TaskNotFoundException if the task was not found + */ + @Mutation + @Description("Update a task") + public Task updateTask(@Name("id") String id, + @Name("description") String description, + @Name("completed") Boolean completed) throws TaskNotFoundException { + + try { + return tasks.compute(id, (k, v) -> { + Objects.requireNonNull(v); + + if (description != null) { + v.setDescription(description); + } + if (completed != null) { + v.setCompleted(completed); + } + return v; + }); + } catch (Exception e) { + throw new TaskNotFoundException(MESSAGE + id); + } + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java new file mode 100644 index 00000000000..12a4cbe5a63 --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +/** + * An exception indicating that a {@link Task} was not found. + */ +public class TaskNotFoundException extends Exception { + /** + * Create the exception. + * @param message reason for the exception. + */ + public TaskNotFoundException(String message) { + super(message); + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java new file mode 100644 index 00000000000..f7ad3736b35 --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A set of small usage examples. Start with {@link io.helidon.grpc.examples.basics.Main} class. + */ +package io.helidon.examples.graphql.basics; diff --git a/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml b/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e5a9e54aa5e --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..6c47520db6c --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.static.classpath.context=/ui +server.static.classpath.location=/web +graphql.cors=Access-Control-Allow-Origin +mp.graphql.exceptionsWhiteList=java.lang.IllegalArgumentException diff --git a/examples/microprofile/graphql/src/main/resources/logging.properties b/examples/microprofile/graphql/src/main/resources/logging.properties new file mode 100644 index 00000000000..d777fc779e0 --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.common.HelidonConsoleHandler + +io.helidon.common.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING +io.helidon.level=INFO diff --git a/examples/microprofile/graphql/src/main/resources/web/index.html b/examples/microprofile/graphql/src/main/resources/web/index.html new file mode 100644 index 00000000000..f5e712fcb7e --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/web/index.html @@ -0,0 +1,23 @@ + + + + + Sample + +

To enable GraphiQL UI please see the Microprofile GraphQL example README.md

+

+ \ No newline at end of file diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml index 87cfff264cf..da8b3a1b2d8 100644 --- a/examples/microprofile/pom.xml +++ b/examples/microprofile/pom.xml @@ -32,6 +32,7 @@ Helidon Microprofile Examples + graphql hello-world-implicit hello-world-explicit static-content diff --git a/examples/pom.xml b/examples/pom.xml index cfd5baaee7a..8e892d02461 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -40,6 +40,7 @@ config + graphql grpc health quickstarts diff --git a/graphql/pom.xml b/graphql/pom.xml new file mode 100644 index 00000000000..dee45e7e6ce --- /dev/null +++ b/graphql/pom.xml @@ -0,0 +1,40 @@ + + + + + + 4.0.0 + + + helidon-project + io.helidon + 2.1.1-SNAPSHOT + + + io.helidon.graphql + helidon-graphql-project + Helidon GraphQL Project + pom + + + server + + diff --git a/graphql/server/pom.xml b/graphql/server/pom.xml new file mode 100644 index 00000000000..45ad95dc002 --- /dev/null +++ b/graphql/server/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + + io.helidon.graphql + helidon-graphql-project + 2.1.1-SNAPSHOT + + + helidon-graphql-server + Helidon GraphQL Server + + + + io.helidon.media + helidon-media-jsonb + + + io.helidon.webserver + helidon-webserver-cors + + + com.graphql-java + graphql-java + + + + io.helidon.webclient + helidon-webclient + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContext.java b/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContext.java new file mode 100644 index 00000000000..45b986e6f69 --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContext.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +/** + * GraphQL execution context to support partial results. + */ +public interface ExecutionContext { + /** + * Add a partial results {@link Throwable}. + * + * @param throwable {@link Throwable} + */ + void partialResultsException(Throwable throwable); + + /** + * Retrieve partial results {@link Throwable}. + * + * @return the {@link Throwable} + */ + Throwable partialResultsException(); + + /** + * Whether an exception was set on this context. + * + * @return true if there was a partial results exception + */ + boolean hasPartialResultsException(); +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContextImpl.java b/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContextImpl.java new file mode 100644 index 00000000000..cab5f933274 --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/ExecutionContextImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +import java.util.concurrent.atomic.AtomicReference; + +class ExecutionContextImpl implements ExecutionContext { + private final AtomicReference currentThrowable = new AtomicReference<>(); + + ExecutionContextImpl() { + } + + @Override + public void partialResultsException(Throwable throwable) { + currentThrowable.set(throwable); + } + + @Override + public Throwable partialResultsException() { + return currentThrowable.get(); + } + + @Override + public boolean hasPartialResultsException() { + return currentThrowable.get() != null; + } +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlConstants.java b/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlConstants.java new file mode 100644 index 00000000000..a3175b0c6a9 --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlConstants.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +/** + * Constants used across GraphQL implementation. + */ +public final class GraphQlConstants { + /** + * Key for errors. + */ + public static final String ERRORS = "errors"; + + /** + * Key for extensions. + */ + public static final String EXTENSIONS = "extensions"; + + /** + * Key for locations. + */ + public static final String LOCATIONS = "locations"; + + /** + * Key for message. + */ + public static final String MESSAGE = "message"; + + /** + * Key for data. + */ + public static final String DATA = "data"; + + /** + * Key for line. + */ + public static final String LINE = "line"; + + /** + * Key for column. + */ + public static final String COLUMN = "column"; + + /** + * Key for path. + */ + public static final String PATH = "path"; + /** + * Default web context of GraphQl endpoint. + */ + public static final String GRAPHQL_WEB_CONTEXT = "/graphql"; + /** + * Default URI of GraphQl schema under the {@link #GRAPHQL_WEB_CONTEXT}. + */ + public static final String GRAPHQL_SCHEMA_URI = "/schema.graphql"; + /** + * Default error message to return for unchecked exceptions and errors. + */ + public static final String DEFAULT_ERROR_MESSAGE = "Server Error"; + + // forbid instantiation + private GraphQlConstants() { + } +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlSupport.java b/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlSupport.java new file mode 100644 index 00000000000..e5e786ee6a5 --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/GraphQlSupport.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; + +import io.helidon.common.GenericType; +import io.helidon.common.configurable.ServerThreadPoolSupplier; +import io.helidon.common.http.Parameters; +import io.helidon.config.Config; +import io.helidon.media.common.MessageBodyReader; +import io.helidon.media.common.MessageBodyWriter; +import io.helidon.media.jsonb.JsonbSupport; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; +import io.helidon.webserver.cors.CorsEnabledServiceHelper; +import io.helidon.webserver.cors.CrossOriginConfig; + +import graphql.schema.GraphQLSchema; + +import static org.eclipse.yasson.YassonConfig.ZERO_TIME_PARSE_DEFAULTING; + +/** + * Support for GraphQL for Helidon WebServer. + */ +public class GraphQlSupport implements Service { + private static final Logger LOGGER = Logger.getLogger(GraphQlSupport.class.getName()); + private static final Jsonb JSONB = JsonbBuilder.newBuilder() + .withConfig(new JsonbConfig() + .setProperty(ZERO_TIME_PARSE_DEFAULTING, true) + .withNullValues(true).withAdapters()) + .build(); + private static final MessageBodyWriter JSONB_WRITER = JsonbSupport.writer(JSONB); + private static final MessageBodyReader JSONB_READER = JsonbSupport.reader(JSONB); + @SuppressWarnings("rawtypes") + private static final GenericType LINKED_HASH_MAP_GENERIC_TYPE = GenericType.create(LinkedHashMap.class); + + private final String context; + private final String schemaUri; + private final InvocationHandler invocationHandler; + private final CorsEnabledServiceHelper corsEnabled; + private final ExecutorService executor; + + private GraphQlSupport(Builder builder) { + this.context = builder.context; + this.schemaUri = builder.schemaUri; + this.invocationHandler = builder.handler; + this.corsEnabled = CorsEnabledServiceHelper.create("GraphQL", builder.crossOriginConfig); + this.executor = builder.executor.get(); + } + + /** + * Create GraphQL support for a GraphQL schema. + * + * @param schema schema to use for GraphQL + * @return a new support to register with {@link io.helidon.webserver.WebServer} {@link Routing.Builder} + */ + public static GraphQlSupport create(GraphQLSchema schema) { + return builder() + .invocationHandler(InvocationHandler.create(schema)) + .build(); + } + + /** + * A builder for fine grained configuration of the support. + * + * @return a new fluent API builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public void update(Routing.Rules rules) { + // cors + rules.any(context, corsEnabled.processor()); + // schema + rules.get(context + schemaUri, this::graphQlSchema); + // get and post endpoint for graphQL + rules.get(context, this::graphQlGet) + .post(context, this::graphQlPost); + } + + // handle POST request for GraphQL endpoint + private void graphQlPost(ServerRequest req, ServerResponse res) { + JSONB_READER + .read(req.content(), LINKED_HASH_MAP_GENERIC_TYPE, req.content().readerContext()) + .forSingle(entity -> processRequest(res, + (String) entity.get("query"), + (String) entity.get("operationName"), + toVariableMap(entity.get("variables")))) + .exceptionallyAccept(res::send); + } + + // handle GET request for GraphQL endpoint + private void graphQlGet(ServerRequest req, ServerResponse res) { + Parameters queryParams = req.queryParams(); + String query = queryParams.first("query").orElseThrow(() -> new IllegalStateException("Query must be defined")); + String operationName = queryParams.first("operationName").orElse(null); + Map variables = queryParams.first("variables") + .map(this::toVariableMap) + .orElseGet(Map::of); + + processRequest(res, query, operationName, variables); + } + + // handle GET request to obtain GraphQL schema + private void graphQlSchema(ServerRequest req, ServerResponse res) { + res.send(invocationHandler.schemaString()); + } + + private void processRequest(ServerResponse res, + String query, + String operationName, + Map variables) { + executor.submit(() -> { + try { + Map result = invocationHandler.execute(query, operationName, variables); + res.send(JSONB_WRITER.marshall(result)); + } catch (Error e) { + res.send(e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unexpected exception when executing graphQL request", e); + } + }); + + } + + private Map toVariableMap(Object variables) { + if (variables == null) { + return Map.of(); + } + + if (variables instanceof Map) { + Map result = new LinkedHashMap<>(); + Map variablesMap = (Map) variables; + variablesMap.forEach((k, v) -> result.put(String.valueOf(k), v)); + return result; + } else { + return toVariableMap(String.valueOf(variables)); + } + } + + @SuppressWarnings("unchecked") + private Map toVariableMap(String jsonString) { + if (jsonString == null || jsonString.trim().isBlank()) { + return Map.of(); + } + return JSONB.fromJson(jsonString, LinkedHashMap.class); + } + + /** + * Fluent API builder to create {@link io.helidon.graphql.server.GraphQlSupport}. + */ + public static class Builder implements io.helidon.common.Builder { + private String context = GraphQlConstants.GRAPHQL_WEB_CONTEXT; + private String schemaUri = GraphQlConstants.GRAPHQL_SCHEMA_URI; + private CrossOriginConfig crossOriginConfig; + private Supplier executor; + private InvocationHandler handler; + + private Builder() { + } + + @Override + public GraphQlSupport build() { + if (handler == null) { + throw new IllegalStateException("Invocation handler must be defined"); + } + + if (executor == null) { + executor = ServerThreadPoolSupplier.builder() + .name("graphql") + .threadNamePrefix("graphql-") + .build(); + } + + return new GraphQlSupport(this); + } + + /** + * Update builder from configuration. + * + * Configuration options: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Optional configuration parameters
keydefault valuedescription
web-context{@value io.helidon.graphql.server.GraphQlConstants#GRAPHQL_WEB_CONTEXT}Context that serves the GraphQL endpoint.
schema-uri{@value io.helidon.graphql.server.GraphQlConstants#GRAPHQL_SCHEMA_URI}URI that serves the schema (under web context)
corsdefault CORS configurationsee {@link CrossOriginConfig#create(io.helidon.config.Config)}
executor-servicedefault server thread pool configurationsee {@link io.helidon.common.configurable.ServerThreadPoolSupplier#builder()}
+ * + * @param config configuration to use + * @return updated builder instance + */ + public Builder config(Config config) { + config.get("web-context").asString().ifPresent(this::webContext); + config.get("schema-uri").asString().ifPresent(this::schemaUri); + config.get("cors").as(CrossOriginConfig::create).ifPresent(this::crossOriginConfig); + + if (executor == null) { + executor = ServerThreadPoolSupplier.builder() + .name("graphql") + .threadNamePrefix("graphql-") + .config(config.get("executor-service")) + .build(); + } + + return this; + } + + /** + * InvocationHandler to execute GraphQl requests. + * + * @param handler handler to use + * @return updated builder instance + */ + public Builder invocationHandler(InvocationHandler handler) { + this.handler = handler; + return this; + } + + /** + * InvocationHandler to execute GraphQl requests. + * + * @param handler handler to use + * @return updated builder instance + */ + public Builder invocationHandler(Supplier handler) { + return invocationHandler(handler.get()); + } + + /** + * Set a new root context for REST API of graphQL. + * + * @param path context to use + * @return updated builder instance + */ + public Builder webContext(String path) { + if (path.startsWith("/")) { + this.context = path; + } else { + this.context = "/" + path; + } + return this; + } + + /** + * Configure URI that will serve the GraphQL schema under the context root. + * + * @param uri URI of the schema + * @return updated builder instance + */ + public Builder schemaUri(String uri) { + if (uri.startsWith("/")) { + this.schemaUri = uri; + } else { + this.schemaUri = "/" + uri; + } + + return this; + } + + /** + * Set the CORS config from the specified {@code CrossOriginConfig} object. + * + * @param crossOriginConfig {@code CrossOriginConfig} containing CORS set-up + * @return updated builder instance + */ + public Builder crossOriginConfig(CrossOriginConfig crossOriginConfig) { + Objects.requireNonNull(crossOriginConfig, "CrossOriginConfig must be non-null"); + this.crossOriginConfig = crossOriginConfig; + return this; + } + + /** + * Executor service to use for GraphQL processing. + * + * @param executor executor service + * @return updated builder instance + */ + public Builder executor(ExecutorService executor) { + this.executor = () -> executor; + return this; + } + + /** + * Executor service to use for GraphQL processing. + * + * @param executor executor service + * @return updated builder instance + */ + public Builder executor(Supplier executor) { + this.executor = executor; + return this; + } + } +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandler.java b/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandler.java new file mode 100644 index 00000000000..9ba533f1b7f --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandler.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.config.Config; + +import graphql.GraphQL; +import graphql.execution.SubscriptionExecutionStrategy; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaPrinter; + +import static io.helidon.graphql.server.GraphQlConstants.DEFAULT_ERROR_MESSAGE; + +/** + * Invocation handler that allows execution of GraphQL requests without a WebServer. + */ +public interface InvocationHandler { + /** + * Create a handler for GraphQL schema. + * + * @param schema schema to use + * @return a new invocation handler + */ + static InvocationHandler create(GraphQLSchema schema) { + return builder().schema(schema).build(); + } + + /** + * Fluent API builder to configure the invocation handler. + * @return a new builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Execute a GraphQL query. + * + * @param query query string + * @return GraphQL result + */ + default Map execute(String query) { + return execute(query, null, Map.of()); + } + + /** + * Execute a GraphQL query. + * + * @param query query string + * @param operationName operation name + * @param variables variables to use (optional) + * @return GraphQL result + */ + Map execute(String query, String operationName, Map variables); + + /** + * The schema of this GraphQL endpoint. + * + * @return schema as a string + */ + String schemaString(); + + /** + * Configured default error message. + * + * @return default error message + */ + String defaultErrorMessage(); + + /** + * Configured set of exceptions that are blacklisted. + * + * @return blacklisted exception class set + */ + Set blacklistedExceptions(); + + /** + * Configured set of exceptions that are whitelisted. + * + * @return whitelisted exception class set + */ + Set whitelistedExceptions(); + + /** + * Fluent API builder to configure the invocation handler. + */ + class Builder implements io.helidon.common.Builder { + private final Set blacklistedExceptions = new HashSet<>(); + private final Set whitelistedExceptions = new HashSet<>(); + + private String defaultErrorMessage = DEFAULT_ERROR_MESSAGE; + private GraphQLSchema schema; + private SchemaPrinter schemaPrinter; + + private Builder() { + } + + @Override + public InvocationHandler build() { + if (schema == null) { + throw new IllegalStateException("GraphQL schema must be configured"); + } + + GraphQL graphQl = GraphQL.newGraphQL(schema) + .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy()) + .build(); + + SchemaPrinter.Options options = SchemaPrinter.Options + .defaultOptions() + .includeDirectives(false) + .useAstDefinitions(false) + .includeScalarTypes(true); + + schemaPrinter = new SchemaPrinter(options); + + return new InvocationHandlerImpl(this, graphQl); + } + + /** + * Update builder from configuration. + * + * Configuration options: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Optional configuration parameters
keydefault valuedescription
default-error-message{@value io.helidon.graphql.server.GraphQlConstants#DEFAULT_ERROR_MESSAGE}Error message used for internal errors that are not whitelisted.
exception-white-list Array of exceptions classes. If an {@link java.lang.Error} or a {@link java.lang.RuntimeException} + * of this type is caught, its message will be propagated to the caller.
exception-black-list Array of exception classes. If a checked {@link java.lang.Exception} is called, its message + * is propagated to the caller, unless it is in the blacklist.
+ * + * @param config configuration to use + * @return updated builder instance + */ + public Builder config(Config config) { + config.get("default-error-message").asString().ifPresent(this::defaultErrorMessage); + config.get("exception-white-list").asList(String.class) + .stream() + .flatMap(List::stream) + .forEach(this::addWhitelistedException); + config.get("exception-black-list").asList(String.class) + .stream() + .flatMap(List::stream) + .forEach(this::addBlacklistedException); + + return this; + } + + /** + * Configure the GraphQL schema to be used. + * + * @param schema schema to handle by this support + * @return updated builder instance + */ + public Builder schema(GraphQLSchema schema) { + this.schema = schema; + return this; + } + + /** + * Default error message to return when an internal server error occurs. + * + * @param defaultErrorMessage default error message + * @return updated builder instance + */ + public Builder defaultErrorMessage(String defaultErrorMessage) { + this.defaultErrorMessage = defaultErrorMessage; + return this; + } + + /** + * Blacklisted error classes that will not return error message back to caller. + * + * @param classNames names of classes to deny for checked exceptions + * @return updated builder instance + */ + public Builder exceptionBlacklist(String[] classNames) { + for (String className : classNames) { + addBlacklistedException(className); + } + return this; + } + + /** + * Add an exception to the blacklist. If a blacklisted exception is thrown, {@link #defaultErrorMessage(String)} + * is returned instead. + * + * @param exceptionClass exception to blacklist + * @return updated builder instance + */ + public Builder addBlacklistedException(String exceptionClass) { + blacklistedExceptions.add(exceptionClass); + return this; + } + + /** + * Whitelisted error classes that will return error message back to caller. + * + * @param classNames names of classes to allow for runtime exceptions and errors + * @return updated builder instance + */ + public Builder exceptionWhitelist(String[] classNames) { + for (String className : classNames) { + addWhitelistedException(className); + } + return this; + } + + /** + * Add an exception to the whitelist. If a whitelisted exception is thrown, its message is returned, otherwise + * {@link #defaultErrorMessage(String)} is returned. + * + * @param exceptionClass exception to whitelist + * @return updated builder instance + */ + public Builder addWhitelistedException(String exceptionClass) { + whitelistedExceptions.add(exceptionClass); + return this; + } + + GraphQLSchema schema() { + return schema; + } + + String defaultErrorMessage() { + return defaultErrorMessage; + } + + Set denyExceptions() { + return blacklistedExceptions; + } + + Set allowExceptions() { + return whitelistedExceptions; + } + + SchemaPrinter schemaPrinter() { + return schemaPrinter; + } + } +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandlerImpl.java b/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandlerImpl.java new file mode 100644 index 00000000000..5674d38d56e --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/InvocationHandlerImpl.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import graphql.ExceptionWhileDataFetching; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.GraphQLError; +import graphql.GraphQLException; +import graphql.language.SourceLocation; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaPrinter; +import graphql.validation.ValidationError; + +import static io.helidon.graphql.server.GraphQlConstants.COLUMN; +import static io.helidon.graphql.server.GraphQlConstants.DATA; +import static io.helidon.graphql.server.GraphQlConstants.ERRORS; +import static io.helidon.graphql.server.GraphQlConstants.EXTENSIONS; +import static io.helidon.graphql.server.GraphQlConstants.LINE; +import static io.helidon.graphql.server.GraphQlConstants.LOCATIONS; +import static io.helidon.graphql.server.GraphQlConstants.MESSAGE; +import static io.helidon.graphql.server.GraphQlConstants.PATH; + +class InvocationHandlerImpl implements InvocationHandler { + private static final Logger LOGGER = Logger.getLogger(InvocationHandlerImpl.class.getName()); + + private final String defaultErrorMessage; + private final Set exceptionDenySet = new HashSet<>(); + private final Set exceptionAllowSet = new HashSet<>(); + private final Map, Boolean> denyExceptions = new ConcurrentHashMap<>(); + private final Map, Boolean> allowExceptions = new ConcurrentHashMap<>(); + private final GraphQLSchema schema; + private final GraphQL graphQl; + private final SchemaPrinter schemaPrinter; + + InvocationHandlerImpl(InvocationHandler.Builder builder, GraphQL graphQl) { + this.schema = builder.schema(); + this.schemaPrinter = builder.schemaPrinter(); + this.defaultErrorMessage = builder.defaultErrorMessage(); + + this.graphQl = graphQl; + + this.exceptionDenySet.addAll(builder.denyExceptions()); + this.exceptionAllowSet.addAll(builder.allowExceptions()); + } + + @Override + public Map execute(String query, String operationName, Map variables) { + try { + return doExecute(query, operationName, variables); + } catch (RuntimeException e) { + LOGGER.log(Level.FINE, "Failed to execute query " + query, e); + Map result = new HashMap<>(); + addError(result, e, e.getMessage()); + return result; + } + } + + private Map doExecute(String query, String operationName, Map variables) { + ExecutionContext context = new ExecutionContextImpl(); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(query) + .operationName(operationName) + .context(context) + .variables(variables) + .build(); + + ExecutionResult result = graphQl.execute(executionInput); + List errors = result.getErrors(); + + if (errors.isEmpty() && context.hasPartialResultsException()) { + return processPartialResultsException(result, context); + } else if (errors.isEmpty()) { + // no errors + return result.toSpecification(); + } else { + // errors + return processErrors(result, errors); + } + } + + private Map processErrors(ExecutionResult result, List errors) { + Map resultMap = new HashMap<>(); + resultMap.put(DATA, result.getData()); + + boolean hasErrors = false; + for (GraphQLError error : errors) { + if (error instanceof ExceptionWhileDataFetching) { + ExceptionWhileDataFetching e = (ExceptionWhileDataFetching) error; + Throwable cause = e.getException().getCause(); + if (cause instanceof Error) { + // re-throw the error as this should result in 500 from graphQL endpoint + throw (Error) cause; + } + hasErrors = true; + addError(resultMap, error, cause); + } else if (error instanceof ValidationError) { + addError(resultMap, error); + // the spec tests for empty "data" node on validation errors + if (result.getData() == null) { + resultMap.put(DATA, null); + } + + hasErrors = true; + } + } + + if (hasErrors) { + return resultMap; + } else { + return result.toSpecification(); + } + } + + private Map processPartialResultsException(ExecutionResult result, + ExecutionContext context) { + // partial result with errors + Throwable cause = context.partialResultsException().getCause(); + Map resultMap = new HashMap<>(); + resultMap.put(DATA, result.getData()); + addError(resultMap, cause, context.partialResultsException().getMessage()); + return resultMap; + } + + private void addError(Map resultMap, + GraphQLError error, + Throwable cause) { + + int line = -1; + int column = -1; + String path = null; + + List locations = error.getLocations(); + if (locations != null && locations.size() > 0) { + SourceLocation sourceLocation = locations.get(0); + line = sourceLocation.getLine(); + column = sourceLocation.getColumn(); + } + + List listPath = error.getPath(); + if (listPath != null && listPath.size() > 0) { + path = listPath.get(0).toString(); + } + + if (cause instanceof GraphQLException) { + addErrorPayload(resultMap, getCheckedMessage(cause), path, line, column, error.getExtensions()); + } else if (cause instanceof Error || cause instanceof RuntimeException) { + addErrorPayload(resultMap, getUncheckedMessage(cause), path, line, column, error.getExtensions()); + } else { + addErrorPayload(resultMap, + (cause == null) ? error.getMessage() : getCheckedMessage(cause), + path, + line, + column, + error.getExtensions()); + } + } + + private void addError(Map resultMap, + GraphQLError error) { + + int line = -1; + int column = -1; + String path = null; + + List locations = error.getLocations(); + if (locations != null && locations.size() > 0) { + SourceLocation sourceLocation = locations.get(0); + line = sourceLocation.getLine(); + column = sourceLocation.getColumn(); + } + + List listPath = error.getPath(); + if (listPath != null && listPath.size() > 0) { + path = listPath.get(0).toString(); + } + + addErrorPayload(resultMap, error.getMessage(), path, line, column, error.getExtensions()); + } + + @SuppressWarnings("unchecked") + private void addError(Map resultMap, + Throwable cause, + String originalMessage) { + + Object data = resultMap.get(DATA); + String path = null; + + if (data instanceof Map) { + Map dataMap = (Map) data; + path = dataMap.keySet().stream().findFirst().orElse(null); + } + + if (cause instanceof GraphQLException) { + addErrorPayload(resultMap, getCheckedMessage(cause), path); + } else if (cause instanceof Error || cause instanceof RuntimeException) { + addErrorPayload(resultMap, getUncheckedMessage(cause), path); + } else { + addErrorPayload(resultMap, (cause == null) ? originalMessage : getCheckedMessage(cause), path); + } + } + + private void addErrorPayload(Map resultMap, String checkedMessage, String path) { + addErrorPayload(resultMap, checkedMessage, path, -1, -1, Map.of()); + } + + @SuppressWarnings("unchecked") + private void addErrorPayload(Map resultMap, + String message, + String path, + int line, + int column, + Map extensions) { + LinkedList> errorList = (LinkedList>) resultMap + .computeIfAbsent(ERRORS, it -> new LinkedList>()); + + Map newErrorMap = new HashMap<>(); + // inner map + newErrorMap.put(MESSAGE, message); + + if (line != -1 && column != -1) { + ArrayList> listLocations = new ArrayList<>(); + listLocations.add(Map.of(LINE, line, COLUMN, column)); + newErrorMap.put(LOCATIONS, listLocations); + } + + if (extensions != null && extensions.size() > 0) { + newErrorMap.put(EXTENSIONS, extensions); + } + + if (path != null) { + newErrorMap.put(PATH, List.of(path)); + } + + errorList.add(newErrorMap); + } + + private String getUncheckedMessage(Throwable throwable) { + Class exceptionClazz = throwable.getClass(); + + if (allowExceptions.containsKey(exceptionClazz)) { + return throwable.getMessage(); + } + + // the allow list is exception or its superclass + // we do not want to use class.forName, as that causes trouble in native image + do { + if (exceptionAllowSet.contains(exceptionClazz.getName())) { + allowExceptions.put(exceptionClazz, true); + return throwable.getMessage(); + } + exceptionClazz = exceptionClazz.getSuperclass(); + } while (exceptionClazz != null); + + return defaultErrorMessage; + } + + private String getCheckedMessage(Throwable throwable) { + Class exceptionClazz = throwable.getClass(); + + // loop through each exception in the deny list and check if + // the exception on the deny list or a subclass of an exception on the deny list + if (denyExceptions.containsKey(exceptionClazz)) { + // cache + return defaultErrorMessage; + } + + // the deny list is exception or its superclass + // we do not want to use class.forName, as that causes trouble in native image + do { + if (exceptionDenySet.contains(exceptionClazz.getName())) { + denyExceptions.put(exceptionClazz, true); + return defaultErrorMessage; + } + exceptionClazz = exceptionClazz.getSuperclass(); + } while (exceptionClazz != null); + + return throwable.getMessage(); + } + + @Override + public String schemaString() { + return schemaPrinter.print(schema); + } + + @Override + public String defaultErrorMessage() { + return defaultErrorMessage; + } + + @Override + public Set blacklistedExceptions() { + return Collections.unmodifiableSet(exceptionDenySet); + } + + @Override + public Set whitelistedExceptions() { + return Collections.unmodifiableSet(exceptionAllowSet); + } +} diff --git a/graphql/server/src/main/java/io/helidon/graphql/server/package-info.java b/graphql/server/src/main/java/io/helidon/graphql/server/package-info.java new file mode 100644 index 00000000000..5b6d2b2fa27 --- /dev/null +++ b/graphql/server/src/main/java/io/helidon/graphql/server/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * GraphQL server implementation for Helidon SE. + * + * @see io.helidon.graphql.server.GraphQlSupport + */ +package io.helidon.graphql.server; diff --git a/graphql/server/src/main/java/module-info.java b/graphql/server/src/main/java/module-info.java new file mode 100644 index 00000000000..5a9a803e031 --- /dev/null +++ b/graphql/server/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * GraphQl server implementation. + */ +module io.helidon.graphql.server { + requires java.logging; + + requires java.json.bind; + requires org.eclipse.yasson; + + requires io.helidon.common.configurable; + requires io.helidon.common.http; + requires io.helidon.media.common; + requires io.helidon.media.jsonb; + requires io.helidon.webserver; + + requires transitive io.helidon.webserver.cors; + requires transitive io.helidon.config; + requires transitive graphql.java; + + exports io.helidon.graphql.server; +} \ No newline at end of file diff --git a/graphql/server/src/test/java/io/helidon/graphql/server/GraphQlSupportTest.java b/graphql/server/src/test/java/io/helidon/graphql/server/GraphQlSupportTest.java new file mode 100644 index 00000000000..8bbdbd0b54f --- /dev/null +++ b/graphql/server/src/test/java/io/helidon/graphql/server/GraphQlSupportTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.graphql.server; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.helidon.media.jsonb.JsonbSupport; +import io.helidon.webclient.WebClient; +import io.helidon.webserver.Routing; +import io.helidon.webserver.WebServer; + +import graphql.schema.GraphQLSchema; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +class GraphQlSupportTest { + + @SuppressWarnings("unchecked") + @Test + void testHelloWorld() { + WebServer server = WebServer.builder() + .routing(Routing.builder() + .register(GraphQlSupport.create(buildSchema())) + .build()) + .build() + .start() + .await(10, TimeUnit.SECONDS); + + WebClient webClient = WebClient.builder() + .addMediaSupport(JsonbSupport.create()) + .build(); + + LinkedHashMap response = webClient + .post() + .uri("http://localhost:" + server.port() + "/graphql") + .submit("{\"query\": \"{hello}\"}", LinkedHashMap.class) + .await(10, TimeUnit.SECONDS); + + Map data = (Map) response.get("data"); + assertThat("POST errors: " + response.get("errors"), data, notNullValue()); + assertThat("POST", data.get("hello"), is("world")); + + response = webClient + .get() + .uri("http://localhost:" + server.port() + "/graphql") + .queryParam("query", "{hello}") + .request(LinkedHashMap.class) + .await(10, TimeUnit.SECONDS); + + data = (Map) response.get("data"); + assertThat("GET errors: " + response.get("errors"), data, notNullValue()); + assertThat("GET", data.get("hello"), is("world")); + + server.shutdown(); + } + + private static GraphQLSchema buildSchema() { + String schema = "type Query{hello: String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world"))) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } +} \ No newline at end of file diff --git a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java index ea7ec286378..bf8732f191a 100644 --- a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java +++ b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java @@ -145,7 +145,11 @@ private HelidonContainerImpl init() { addHelidonBeanDefiningAnnotations("javax.ws.rs.Path", "javax.ws.rs.ext.Provider", - "javax.websocket.server.ServerEndpoint"); + "javax.websocket.server.ServerEndpoint", + "org.eclipse.microprofile.graphql.GraphQLApi", + "org.eclipse.microprofile.graphql.Input", + "org.eclipse.microprofile.graphql.Interface", + "org.eclipse.microprofile.graphql.Type"); ResourceLoader resourceLoader = new WeldResourceLoader() { @Override diff --git a/microprofile/graphql/pom.xml b/microprofile/graphql/pom.xml new file mode 100644 index 00000000000..6fef9abe3c3 --- /dev/null +++ b/microprofile/graphql/pom.xml @@ -0,0 +1,36 @@ + + + + + + io.helidon.microprofile + helidon-microprofile-project + 2.1.1-SNAPSHOT + + 4.0.0 + + io.helidon.microprofile.graphql + helidon-microprofile-graphql + Helidon Microprofile GraphQL + pom + + + server + + + diff --git a/microprofile/graphql/server/pom.xml b/microprofile/graphql/server/pom.xml new file mode 100644 index 00000000000..e3268067657 --- /dev/null +++ b/microprofile/graphql/server/pom.xml @@ -0,0 +1,120 @@ + + + + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql + 2.1.1-SNAPSHOT + + 4.0.0 + + helidon-microprofile-graphql-server + Helidon Microprofile GraphQL Server + The Microprofile GraphQL Server implementation + + + + org.eclipse.microprofile.graphql + microprofile-graphql-api + + + io.helidon.graphql + helidon-graphql-server + + + com.graphql-java + graphql-java-extended-scalars + + + io.helidon.microprofile.server + helidon-microprofile-server + + + io.helidon.microprofile.config + helidon-microprofile-config + + + org.jboss + jandex + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + integration-test + + + + ${project.build.directory}/classes + + + ${project.build.directory}/test-classes + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/AbstractDescriptiveElement.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/AbstractDescriptiveElement.java new file mode 100644 index 00000000000..1b08c839cd3 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/AbstractDescriptiveElement.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.Objects; + +/** + * An abstract implementation of a {@link DescriptiveElement}. + */ +class AbstractDescriptiveElement implements DescriptiveElement { + + /** + * The description for an element. + */ + private String description; + + @Override + public void description(String description) { + this.description = description; + } + + @Override + public String description() { + return this.description; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractDescriptiveElement that = (AbstractDescriptiveElement) o; + return Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(description); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/CustomScalars.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/CustomScalars.java new file mode 100644 index 00000000000..3caf9fdbe7f --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/CustomScalars.java @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; + +import graphql.Scalars; +import graphql.language.StringValue; +import graphql.scalars.ExtendedScalars; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +import static graphql.Scalars.GraphQLBigInteger; +import static graphql.Scalars.GraphQLFloat; +import static graphql.Scalars.GraphQLInt; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_OFFSET_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_ZONED_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.TIME_SCALAR; + +/** + * Custom scalars. + */ +class CustomScalars { + + /** + * Private no-args constructor. + */ + private CustomScalars() { + } + + /** + * An instance of a custome BigDecimal Scalar. + */ + static final GraphQLScalarType CUSTOM_BIGDECIMAL_SCALAR = newCustomBigDecimalScalar(); + + /** + * An instance of a custom Int scalar. + */ + static final GraphQLScalarType CUSTOM_INT_SCALAR = newCustomGraphQLInt(); + + /** + * An instance of a custom Float scalar. + */ + static final GraphQLScalarType CUSTOM_FLOAT_SCALAR = newCustomGraphQLFloat(); + + /** + * An instance of a custom BigInteger scalar. + */ + static final GraphQLScalarType CUSTOM_BIGINTEGER_SCALAR = newCustomGraphQLBigInteger(); + + /** + * An instance of a custom formatted date/time scalar. + */ + static final GraphQLScalarType FORMATTED_CUSTOM_DATE_TIME_SCALAR = newDateTimeScalar(FORMATTED_DATETIME_SCALAR); + + /** + * An instance of a custom formatted time scalar. + */ + static final GraphQLScalarType FORMATTED_CUSTOM_TIME_SCALAR = newTimeScalar(FORMATTED_TIME_SCALAR); + + /** + * An instance of a custom formatted date scalar. + */ + static final GraphQLScalarType FORMATTED_CUSTOM_DATE_SCALAR = newDateScalar(FORMATTED_DATE_SCALAR); + + /** + * An instance of a custom date/time scalar (with default formatting). + */ + static final GraphQLScalarType CUSTOM_DATE_TIME_SCALAR = newDateTimeScalar(DATETIME_SCALAR); + + /** + * An instance of a custom offset date/time scalar (with default formatting). + */ + static final GraphQLScalarType CUSTOM_OFFSET_DATE_TIME_SCALAR = + newOffsetDateTimeScalar(FORMATTED_OFFSET_DATETIME_SCALAR); + + /** + * An instance of a custom offset date/time scalar (with default formatting). + */ + static final GraphQLScalarType CUSTOM_ZONED_DATE_TIME_SCALAR = + newZonedDateTimeScalar(FORMATTED_ZONED_DATETIME_SCALAR); + + /** + * An instance of a custom time scalar (with default formatting). + */ + static final GraphQLScalarType CUSTOM_TIME_SCALAR = newTimeScalar(TIME_SCALAR); + + /** + * An instance of a custom date scalar (with default formatting). + */ + static final GraphQLScalarType CUSTOM_DATE_SCALAR = newDateScalar(DATE_SCALAR); + + /** + * Return a new custom date/time scalar. + * + * @param name the name of the scalar + * @return a new custom date/time scalar + */ + static GraphQLScalarType newDateTimeScalar(String name) { + GraphQLScalarType originalScalar = ExtendedScalars.DateTime; + + return GraphQLScalarType.newScalar() + .coercing(new DateTimeCoercing()) + .name(name) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom offset date/time scalar. + * + * @param name the name of the scalar + * @return a new custom date/time scalar + */ + @SuppressWarnings("unchecked") + static GraphQLScalarType newOffsetDateTimeScalar(String name) { + GraphQLScalarType originalScalar = ExtendedScalars.DateTime; + + return GraphQLScalarType.newScalar() + .coercing(new DateTimeCoercing()) + .name(name) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom zoned date/time scalar. + * + * @param name the name of the scalar + * @return a new custom date/time scalar + */ + @SuppressWarnings("unchecked") + static GraphQLScalarType newZonedDateTimeScalar(String name) { + GraphQLScalarType originalScalar = ExtendedScalars.DateTime; + + return GraphQLScalarType.newScalar() + .coercing(new DateTimeCoercing()) + .name(name) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom time scalar. + * + * @param name the name of the scalar + * @return a new custom time scalar + */ + static GraphQLScalarType newTimeScalar(String name) { + GraphQLScalarType originalScalar = ExtendedScalars.Time; + + return GraphQLScalarType.newScalar() + .coercing(new TimeCoercing()) + .name(name) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom date scalar. + * + * @param name the name of the scalar + * @return a new custom date scalar + */ + static GraphQLScalarType newDateScalar(String name) { + GraphQLScalarType originalScalar = ExtendedScalars.Date; + return GraphQLScalarType.newScalar() + .coercing(new DateTimeCoercing()) + .name(name) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom BigDecimal scalar. + * + * @return a new custom BigDecimal scalar + */ + private static GraphQLScalarType newCustomBigDecimalScalar() { + GraphQLScalarType originalScalar = Scalars.GraphQLBigDecimal; + + return GraphQLScalarType.newScalar() + .coercing(new NumberCoercing(originalScalar.getCoercing())) + .name(originalScalar.getName()) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom Int scalar. + * + * @return a new custom Int scalar + */ + private static GraphQLScalarType newCustomGraphQLInt() { + GraphQLScalarType originalScalar = GraphQLInt; + + return GraphQLScalarType.newScalar() + .coercing(new NumberCoercing(originalScalar.getCoercing())) + .name(originalScalar.getName()) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom Float scalar. + * + * @return a new custom Float scalar + */ + private static GraphQLScalarType newCustomGraphQLFloat() { + GraphQLScalarType originalScalar = GraphQLFloat; + + return GraphQLScalarType.newScalar() + .coercing(new NumberCoercing(originalScalar.getCoercing())) + .name(originalScalar.getName()) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Return a new custom BigInteger scalar. + * + * @return a new custom BigInteger scalar + */ + private static GraphQLScalarType newCustomGraphQLBigInteger() { + GraphQLScalarType originalScalar = GraphQLBigInteger; + + return GraphQLScalarType.newScalar() + .coercing(new NumberCoercing(originalScalar.getCoercing())) + .name(originalScalar.getName()) + .description("Custom: " + originalScalar.getDescription()) + .build(); + } + + /** + * Abstract implementation of {@link Coercing} interface for given classes. + */ + abstract static class AbstractDateTimeCoercing implements Coercing { + + /** + * {@link Class}es that can be coerced. + */ + private final Class[] clazzes; + + /** + * Construct a {@link AbstractDateTimeCoercing}. + * + * @param clazzes {@link Class}es to coerce + */ + AbstractDateTimeCoercing(Class... clazzes) { + this.clazzes = clazzes; + } + + @Override + public Object serialize(Object dataFetcherResult) throws CoercingSerializeException { + return convert(dataFetcherResult); + } + + @Override + public Object parseValue(Object input) throws CoercingParseValueException { + return convert(input); + } + + @Override + public Object parseLiteral(Object input) throws CoercingParseLiteralException { + return parseStringLiteral(input); + } + + /** + * Convert the given input to the type of if a String then leave it be. + * + * @param input input to coerce + * @return the coerced value + * @throws CoercingParseLiteralException if any exceptions converting + */ + private Object convert(Object input) throws CoercingParseLiteralException { + if (input instanceof String) { + return input; + } + + for (Class clazz : clazzes) { + if (clazz.isInstance(input)) { + return input; + } + } + + throw new CoercingParseLiteralException("Unable to convert type of " + input.getClass()); + } + + /** + * Parse a String literal and return instance of {@link StringValue} or throw an exception. + * + * @param input input to parse + * @throws CoercingParseLiteralException if it is not a {@link StringValue} + */ + private String parseStringLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException("Expected AST type 'StringValue' but was '" + + ( + input == null + ? "null" + : input.getClass().getSimpleName()) + "'."); + } + return ((StringValue) input).getValue(); + } + } + + /** + * Coercing Implementation for Date/Time. + */ + static class DateTimeCoercing extends AbstractDateTimeCoercing { + + /** + * Construct a {@link DateTimeCoercing}. + */ + DateTimeCoercing() { + super(LocalDateTime.class, OffsetDateTime.class, ZonedDateTime.class); + } + } + + /** + * Coercing implementation for Time. + */ + static class TimeCoercing extends AbstractDateTimeCoercing { + + /** + * Construct a {@link TimeCoercing}. + */ + TimeCoercing() { + super(LocalTime.class, OffsetTime.class); + } + } + + /** + * Coercing implementation for Date. + */ + static class DateCoercing extends AbstractDateTimeCoercing { + + /** + * Construct a {@link DateCoercing}. + */ + DateCoercing() { + super(LocalDate.class); + } + } + + /** + * Coercing implementation for BigDecimal. + */ + static class BigDecimalCoercing extends AbstractDateTimeCoercing { + + /** + * Construct a {@link DateCoercing}. + */ + BigDecimalCoercing() { + super(BigDecimal.class); + } + } + + /** + * Number implementation of {@link Coercing} interface for given classes. + * @param defines input type + */ + @SuppressWarnings("unchecked") + static class NumberCoercing implements Coercing { + + /** + * Original {@link Coercing} to fall back on if neeed. + */ + private final Coercing originalCoercing; + + /** + * Construct a {@link NumberCoercing} from an original {@link Coercing}. + * + * @param originalCoercing original {@link Coercing} + */ + NumberCoercing(Coercing originalCoercing) { + this.originalCoercing = originalCoercing; + } + + @Override + public Object serialize(Object dataFetcherResult) throws CoercingSerializeException { + return dataFetcherResult instanceof String + ? (String) dataFetcherResult + : originalCoercing.serialize(dataFetcherResult); + } + + @Override + public I parseValue(Object input) throws CoercingParseValueException { + return (I) originalCoercing.parseValue(input); + } + + @Override + public I parseLiteral(Object input) throws CoercingParseLiteralException { + return (I) originalCoercing.parseLiteral(input); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DataFetcherUtils.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DataFetcherUtils.java new file mode 100644 index 00000000000..336ba2c3ade --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DataFetcherUtils.java @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.NumberFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.logging.Logger; + +import javax.enterprise.inject.spi.CDI; + +import io.helidon.graphql.server.ExecutionContext; + +import graphql.GraphQLException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.PropertyDataFetcher; +import graphql.schema.PropertyDataFetcherHelper; + +import static io.helidon.microprofile.graphql.server.FormattingHelper.formatDate; +import static io.helidon.microprofile.graphql.server.FormattingHelper.formatNumber; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectDateFormatter; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectNumberFormat; +import static io.helidon.microprofile.graphql.server.SchemaGenerator.isFormatEmpty; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ID; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureRuntimeException; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isDateTimeClass; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isPrimitiveArray; + +/** + * Utilities for working with {@link DataFetcher}s. + */ +class DataFetcherUtils { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(DataFetcherUtils.class.getName()); + + /** + * Empty format. + */ + private static final String[] EMPTY_FORMAT = new String[] {null, null }; + + /** + * Map message. + */ + private static final String MAP_MESSAGE = "This implementation does not support using a Map " + + "as input to a query or mutation"; + + /** + * Private constructor for utilities class. + */ + private DataFetcherUtils() { + } + + /** + * Create a new {@link DataFetcher} for a {@link Class} and {@link Method} to be executed. + * + * @param clazz {@link Class} to call + * @param method {@link Method} to call + * @param source defines the source for a @Source annotation - may be null + * @param args optional {@link SchemaArgument}s + * @param schema {@link Schema} that created this {@link DataFetcher} + * @param value type + * @return a new {@link DataFetcher} + */ + @SuppressWarnings("unchecked") + static DataFetcher newMethodDataFetcher(Schema schema, Class clazz, Method method, + String source, SchemaArgument... args) { + + // this is an application scoped bean + GraphQlBean bean = CDI.current().select(GraphQlBean.class).get(); + + return environment -> { + ArrayList listArgumentValues = new ArrayList<>(); + // only one @Source annotation should be present and it should be the first argument + if (source != null) { + Class sourceClazz; + try { + sourceClazz = Class.forName(source); + listArgumentValues.add(sourceClazz.cast(environment.getSource())); + } catch (ClassNotFoundException e) { + LOGGER.warning("Unable to find source class " + source); + } + } + + if (args.length > 0) { + for (SchemaArgument argument : args) { + // ensure a Map is not used as an input type + Class originalType = argument.originalType(); + if (originalType != null && Map.class.isAssignableFrom(originalType)) { + ensureRuntimeException(LOGGER, MAP_MESSAGE); + } + + if (argument.isArrayReturnType() && argument.arrayLevels() > 1 + && SchemaGeneratorHelper.isPrimitiveArray(argument.originalType())) { + throw new GraphQlConfigurationException("This implementation does not currently support " + + "multi-level primitive arrays as arguments. Please use " + + "List or Collection of Object equivalent. E.g. " + + "In place of method(int [][] value) use " + + " method(List> value)"); + } + + listArgumentValues.add(generateArgumentValue(schema, argument.argumentType(), + argument.originalType(), + argument.originalArrayType(), + environment.getArgument(argument.argumentName()), + argument.format())); + } + } + + try { + // this is the right place to validate security + return (V) bean.runGraphQl(clazz, method, listArgumentValues.toArray()); + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + GraphQLException exception = new GraphQLException(e.getTargetException()); + if (targetException instanceof org.eclipse.microprofile.graphql.GraphQLException) { + // if we have partial results we need to return those results and they will + // get converted correctly to the format required by GraphQL and the ExecutionContext.execute() + // we ensure this is throw correctly as an error + ExecutionContext context = environment.getContext(); + context.partialResultsException(exception); + return (V) ((org.eclipse.microprofile.graphql.GraphQLException) targetException).getPartialResults(); + } + throw exception; + } + }; + } + + /** + * Return a {@link DataFetcher} which converts a {@link Map} to a {@link Collection} of V. + * This assumes that the key for the {@link Map} is contained within the V + * + * @param propertyName name of the property to apply to + * @param Source of the property + * @param type of the value + * @return a {@link DataFetcher} + */ + @SuppressWarnings("unchecked") + static DataFetcher> newMapValuesDataFetcher(String propertyName) { + return environment -> { + S source = environment.getSource(); + if (source == null) { + return null; + } + + // retrieve the map and return the collection of V + Map map = (Map) PropertyDataFetcherHelper + .getPropertyValue(propertyName, source, environment.getFieldType(), environment); + return map.values(); + }; + } + + /** + * Generate an argument value with the given information. This may be called recursively. + * + * @param schema {@link Schema} to introspect if needed + * @param argumentType the type of the argument + * @param originalType if this is non null this means the array was a Collection and this is the type in the collection + * @param originalArrayType the original type of the argument as a class + * @param rawValue raw value of the argument + * @param format argument format + * @return the argument value + * @throws Exception if any errors + */ + @SuppressWarnings({"unchecked", "rawtypes" }) + protected static Object generateArgumentValue(Schema schema, String argumentType, Class originalType, + Class originalArrayType, + Object rawValue, String[] format) + throws Exception{ + if (rawValue instanceof Map) { + // this means the type is an input type so convert it to the correct class instance + SchemaInputType inputType = schema.getInputTypeByName(argumentType); + + // loop through the map and convert each entry + Map map = (Map) rawValue; + Map mapConverted = new HashMap<>(); + + for (Map.Entry entry : map.entrySet()) { + // retrieve the Field Definition + String fdName = entry.getKey(); + Object value = entry.getValue(); + SchemaFieldDefinition fd = inputType.getFieldDefinitionByName(fdName); + + // check to see if the Field Definition return type is an input type + SchemaInputType inputFdInputType = schema.getInputTypeByName(fd.returnType()); + if (inputFdInputType != null && value instanceof Map) { + mapConverted.put(fdName, generateArgumentValue(schema, inputFdInputType.name(), + Class.forName(inputFdInputType.valueClassName()), + null, + value, EMPTY_FORMAT)); + } else { + if (fd.isJsonbFormat() || fd.isJsonbProperty()) { + // don't deserialize using formatting as Jsonb will do this for us + mapConverted.put(fdName, value); + } else { + // check it is not a Map + Class originalFdlType = fd.originalType(); + if (originalFdlType != null && originalFdlType.isAssignableFrom(Map.class)) { + ensureRuntimeException(LOGGER, MAP_MESSAGE); + } + // retrieve the data fetcher and check if the property name is different as this should be used + DataFetcher dataFetcher = fd.dataFetcher(); + if (dataFetcher instanceof PropertyDataFetcher) { + fdName = ((PropertyDataFetcher) dataFetcher).getPropertyName(); + } + mapConverted.put(fdName, generateArgumentValue(schema, fd.returnType(), fd.originalType(), + fd.originalArrayType(), value, fd.format())); + } + } + } + + return JsonUtils.convertFromJson(JsonUtils.convertMapToJson(mapConverted), originalType); + + } else if (rawValue instanceof Collection) { + SchemaInputType inputType = schema.getInputTypeByName(argumentType); + + Object colResults = null; + boolean isArray = originalType.isArray(); + try { + if (originalType.equals(List.class) || isArray) { + colResults = new ArrayList<>(); + } else if (originalType.equals(Set.class) || originalType.equals(Collection.class)) { + colResults = new TreeSet<>(); + } else { + Constructor constructor = originalType.getDeclaredConstructor(); + colResults = constructor.newInstance(); + } + } catch (Exception e) { + ensureRuntimeException(LOGGER, "Unable to construct a List of type " + originalType + + " using Collection constructor", e); + } + + if (inputType != null) { + // handle complex types + for (Object value : (Collection) rawValue) { + ((Collection) colResults).add(generateArgumentValue(schema, inputType.name(), + originalArrayType, null, + value, EMPTY_FORMAT)); + } + + if (isArray) { + return ((List) colResults).toArray((Object[]) Array.newInstance(originalType.getComponentType(), 0)); + } else { + return colResults; + } + } else { + // standard type or scalar so ensure we preserve the order and + // convert any values with formats + for (Object value : (Collection) rawValue) { + if (value instanceof Collection) { + ((Collection) colResults).add(generateArgumentValue(schema, argumentType, + originalType, originalArrayType, value, format)); + } else { + ((Collection) colResults).add(parseArgumentValue(originalArrayType, argumentType, value, format)); + } + } + + if (isArray) { + if (isPrimitiveArray(originalType)) { + // array of primitives + return generatePrimitiveArray((List) colResults, originalType, argumentType, format); + } else { + // array of Objects + return ((List) colResults).toArray((Object[]) Array.newInstance(originalType.getComponentType(), 0)); + } + } + if (originalType.equals(List.class)) { + return (List) colResults; + } + if (originalType.equals(Collection.class)) { + return (Collection) colResults; + } + return colResults; + } + } else { + return parseArgumentValue(originalType, argumentType, rawValue, format); + } + } + + /** + * Return an array of primitives of the correct type from the given {@link List} of primitives. + * @param results results to process + * @param originalType the original type + * @param argumentType argument type + * @param format format + * @return an array of primitives of the correct type + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static Object generatePrimitiveArray(List results, Class originalType, String argumentType, String[] format) { + Class componentType = originalType.getComponentType(); + try { + int i = 0; + if (componentType.equals(byte.class)) { + byte[] result = new byte[results.size()]; + for (Object value : results) { + result[i++] = Byte.parseByte(value.toString()); // this will come in as an Integer + } + return result; + } else if (componentType.equals(char.class)) { + char[] result = new char[results.size()]; + for (Object value : results) { + result[i++] = value.toString().charAt(0); + } + return result; + } else if (componentType.equals(boolean.class)) { + boolean[] result = new boolean[results.size()]; + for (Object value : results) { + result[i++] = Boolean.parseBoolean(value.toString()); + } + return result; + } else if (componentType.equals(short.class)) { + short[] result = new short[results.size()]; + for (Object value : results) { + result[i++] = (short) parseArgumentValue(componentType, argumentType, value, format); + } + return result; + } else if (componentType.equals(float.class)) { + float[] result = new float[results.size()]; + for (Object value : results) { + result[i++] = (float) parseArgumentValue(componentType, argumentType, value, format); + } + return result; + } else if (componentType.equals(int.class)) { + int[] result = new int[results.size()]; + for (Object value : results) { + result[i++] = (int) parseArgumentValue(componentType, argumentType, value, format); + } + return result; + } else if (componentType.equals(long.class)) { + long[] result = new long[results.size()]; + for (Object value : results) { + result[i++] = (long) parseArgumentValue(componentType, argumentType, value, format); + } + return result; + } else if (componentType.equals(double.class)) { + double[] result = new double[results.size()]; + for (Object value : results) { + result[i++] = (double) parseArgumentValue(componentType, argumentType, value, format); + } + return result; + } + } catch (Exception e) { + // this exception will get caught by GraphQL and packaged up + throw new RuntimeException(e); + } + return null; + } + + /** + * Parse the given {@link SchemaArgument} and key and return the correct value to match the method argument. + * + * @param originalType original type + * @param argumentType argument type + * @param rawValue the raw value + * @param format format + * @return the parsed value or the original value if no change + * @throws Exception if any errors + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected static Object parseArgumentValue(Class originalType, String argumentType, Object rawValue, String[] format) + throws Exception { + + if (originalType == null) { + // is array type + originalType = rawValue.getClass(); + } + if (originalType.isEnum()) { + Class enumClass = (Class) originalType; + return Enum.valueOf(enumClass, rawValue.toString()); + } else if (argumentType.equals(ID) && rawValue != null) { + // convert back to original data type + return getOriginalIDValue(originalType, rawValue.toString()); + } else { + // check the format and convert it from a string to the original format + if (!isFormatEmpty(format)) { + if (rawValue == null) { + return null; + } else { + if (isDateTimeClass(originalType)) { + DateTimeFormatter dateFormatter = getCorrectDateFormatter(originalType.getName(), + format[1], format[0]); + return getOriginalDateTimeValue(originalType, dateFormatter.parse(rawValue.toString())); + } else { + NumberFormat numberFormat = getCorrectNumberFormat(originalType.getName(), + format[1], format[0]); + if (numberFormat != null) { + return getOriginalValue(originalType, numberFormat.parse(rawValue.toString())); + } else { + return rawValue; + } + } + } + } else { + // process value in case we have to convert between say a Double/Float as they are interchangeable + return getOriginalValue(originalType, rawValue); + } + } + } + + /** + * An implementation of a {@link PropertyDataFetcher} which returns a formatted number. + */ + @SuppressWarnings("rawtypes") + static class NumberFormattingDataFetcher extends PropertyDataFetcher { + + /** + * {@link NumberFormat} to format with. + */ + private NumberFormat numberFormat; + + /** + * Indicates if this is a scalar. + */ + private boolean isScalar; + + /** + * Construct a new NumberFormattingDataFetcher. + * + * @param propertyName property to extract + * @param type GraphQL type of the property + * @param valueFormat formatting value + * @param locale formatting locale + */ + NumberFormattingDataFetcher(String propertyName, String type, String valueFormat, String locale) { + super(propertyName); + numberFormat = getCorrectNumberFormat(type, locale, valueFormat); + if (numberFormat == null) { + ensureRuntimeException(LOGGER, "Unable to get number format for property '" + + propertyName + "' and type '" + type + "'"); + } + isScalar = SchemaGeneratorHelper.getScalar(type) != null; + } + + @Override + public Object get(DataFetchingEnvironment environment) { + return formatNumber(super.get(environment), isScalar, numberFormat); + } + } + + /** + * An implementation of a {@link PropertyDataFetcher} which returns a formatted date. + */ + @SuppressWarnings({ "rawtypes" }) + static class DateFormattingDataFetcher extends PropertyDataFetcher { + + /** + * {@link DateTimeFormatter} to format with. + */ + private DateTimeFormatter dateTimeFormatter; + + /** + * Construct a new DateFormattingDataFetcher. + * + * @param propertyName property to extract + * @param type GraphQL type of the property + * @param valueFormat formatting value + * @param locale formatting locale + */ + DateFormattingDataFetcher(String propertyName, String type, String valueFormat, String locale) { + super(propertyName); + dateTimeFormatter = getCorrectDateFormatter(type, locale, valueFormat); + } + + @Override + public Object get(DataFetchingEnvironment environment) { + return formatDate(super.get(environment), dateTimeFormatter); + } + } + + /** + * Convert the ID type back to the original type for the method call. + * + * @param originalType original type + * @param key the key value passed in + * @return the value as the original type + */ + private static Object getOriginalIDValue(Class originalType, String key) { + if (originalType.equals(Long.class) || originalType.equals(long.class)) { + return Long.parseLong(key); + } else if (originalType.equals(Integer.class) || originalType.equals(int.class)) { + return Integer.parseInt(key); + } else if (originalType.equals(java.util.UUID.class)) { + return UUID.fromString(key); + } else { + return key; + } + } + + /** + * Convert the Object back to the original type for the method call. + * + * @param originalType original type + * @param key the key value passed in + * @return the value as the original type + */ + private static Object getOriginalValue(Class originalType, Object key) { + Number numberKey = null; + if (key instanceof Number) { + // is a number that has been un-formatted + numberKey = (Number) key; + } else if (key instanceof String && originalType.isAssignableFrom(Number.class)) { + // Is a number that has had no format + try { + Constructor constructor = originalType.getDeclaredConstructor(String.class); + numberKey = (Number) constructor.newInstance(key); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException + | InvocationTargetException eIgnore) { + LOGGER.warning("Cannot find constructor with String arg for class " + originalType.getName()); + } + } + + if (numberKey != null) { + return originalType.equals(Float.class) ? Float.valueOf(numberKey.floatValue()) + : originalType.equals(float.class) ? numberKey.floatValue() + : originalType.equals(Integer.class) ? Integer.valueOf(numberKey.intValue()) + : originalType.equals(int.class) ? numberKey.intValue() + : originalType.equals(Long.class) ? Long.valueOf(numberKey.longValue()) + : originalType.equals(long.class) ? numberKey.longValue() + : originalType.equals(Double.class) ? Double.valueOf(numberKey.doubleValue()) + : originalType.equals(double.class) ? numberKey.doubleValue() + : originalType.equals(Byte.class) ? Byte.valueOf(numberKey.byteValue()) + : originalType.equals(byte.class) ? numberKey.byteValue() + : originalType.equals(Short.class) ? Short.valueOf(numberKey.shortValue()) + : originalType.equals(short.class) ? numberKey.shortValue() + : originalType.equals(BigDecimal.class) ? BigDecimal.valueOf(numberKey.doubleValue()) + : originalType.equals(BigInteger.class) ? BigInteger.valueOf(numberKey.longValue()) + : key; + } + if (originalType.equals(Float.class) || originalType.equals(float.class)) { + return (Float) key; + } else if (originalType.equals(Long.class) || originalType.equals(long.class)) { + return Long.valueOf(key.toString()); + } + + return key; + } + + /** + * Return the original date/time value. + * + * @param originalType original type + * @param value the {@link TemporalAccessor} value + * @return the original date/time value + */ + private static TemporalAccessor getOriginalDateTimeValue(Class originalType, TemporalAccessor value) { + if (originalType.equals(LocalDateTime.class)) { + return LocalDateTime.from(value); + } else if (originalType.equals(LocalDate.class)) { + return LocalDate.from(value); + } else if (originalType.equals(ZonedDateTime.class)) { + return ZonedDateTime.from(value); + } else if (originalType.equals(OffsetDateTime.class)) { + return OffsetDateTime.from(value); + } else if (originalType.equals(LocalTime.class)) { + return LocalTime.from(value); + } else { + return null; + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DescriptiveElement.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DescriptiveElement.java new file mode 100644 index 00000000000..9173afc15d0 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/DescriptiveElement.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import static io.helidon.microprofile.graphql.server.ElementGenerator.NEWLINE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.NOTHING; +import static io.helidon.microprofile.graphql.server.ElementGenerator.QUOTE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.TRIPLE_QUOTE; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDefaultDescription; + +/** + * Describes an element that has a description. + */ +interface DescriptiveElement { + + /** + * Set the description for this element. + * + * @param description the description for this element + */ + void description(String description); + + /** + * Return the description for this element. + * + * @return the description for this element + */ + String description(); + + /** + * Return the description of the schema element. Only valid for Type, Field, Method, Parameter + * + * @param format the format for the element + * @return the description of the schema element. + */ + default String getSchemaElementDescription(String[] format) { + String description = getDefaultDescription(format, description()); + + if (description != null) { + // if the description contains a quote or newline then use the triple quote option + boolean useNormalQuotes = description.indexOf('\n') == -1 && description.indexOf('"') == -1; + + return new StringBuilder() + .append(useNormalQuotes + ? QUOTE : (TRIPLE_QUOTE + NEWLINE)) + .append(description) + .append(useNormalQuotes + ? QUOTE : (NEWLINE + TRIPLE_QUOTE)) + .append(NEWLINE) + .toString(); + } + return NOTHING; + } + + /** + * Repeat a {@link String} with the value repeated the requested number of times. + * + * @param count number of times to repeat + * @param string {@link String} to repeat + * @return a new {@link String} + */ + default String repeat(int count, String string) { + return new String(new char[count]).replace("\0", string); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/ElementGenerator.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/ElementGenerator.java new file mode 100644 index 00000000000..8fa5e321117 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/ElementGenerator.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.logging.Logger; + +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BOOLEAN; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureRuntimeException; + +/** + * An interface representing a class which can generate a GraphQL representation of it's state. + */ +interface ElementGenerator { + + /** + * Logger. + */ + Logger LOGGER = Logger.getLogger(ElementGenerator.class.getName()); + + /** + * Empty string. + */ + String NOTHING = ""; + + /** + * Comma and space. + */ + String COMMA_SPACE = ", "; + + /** + * Comma. + */ + String COMMA = ", "; + + /** + * Triple quote. + */ + String TRIPLE_QUOTE = "\"\"\""; + + /** + * Double quote. + */ + String QUOTE = "\""; + + /** + * Spacer. + */ + String SPACER = " "; + + /** + * Open square bracket. + */ + String OPEN_SQUARE = "["; + + /** + * Close square bracket. + */ + String CLOSE_SQUARE = "]"; + + /** + * Open curly bracket. + */ + String OPEN_CURLY = "{"; + + /** + * Close curly bracket. + */ + String CLOSE_CURLY = "}"; + + /** + * Newline. + */ + char NEWLINE = '\n'; + + /** + * Colon. + */ + char COLON = ':'; + + /** + * Equals. + */ + char EQUALS = '='; + + /** + * Mandatory indicator. + */ + char MANDATORY = '!'; + + /** + * Open parenthesis. + */ + char OPEN_PARENTHESES = '('; + + /** + * Close parenthesis. + */ + char CLOSE_PARENTHESES = ')'; + + /** + * Return the GraphQL schema representation of the element. + * + * @return the GraphQL schema representation of the element. + */ + String getSchemaAsString(); + + /** + * Generate a default value for an argument type. + * + * @param defaultValue default value + * @param argumentType argument type + * @return the generated default value + */ + default String generateDefaultValue(Object defaultValue, String argumentType) { + StringBuilder sb = new StringBuilder(); + Object finalDefaultValue = defaultValue; + sb.append(SPACER) + .append(EQUALS) + .append(SPACER); + + boolean isJson = false; + + // check for JSON + if (defaultValue instanceof String) { + String stringDefault = (String) defaultValue; + if (stringDefault.contains(OPEN_CURLY) && stringDefault.contains(CLOSE_CURLY)) { + try { + // is possibly JSON, so convert from JSON to GraphQLSDL format + finalDefaultValue = JsonUtils.convertJsonToGraphQLSDL(JsonUtils.convertJSONtoMap(stringDefault)); + isJson = true; + } catch (Exception e) { + ensureRuntimeException(LOGGER, "Unable to parse default JSON value of" + + "[" + stringDefault + "] for " + this); + } + } + } + // determine how the default value should be rendered + if (FLOAT.equals(argumentType) || INT.equals(argumentType) + || BOOLEAN.equals(argumentType) || BIG_INTEGER.equals(argumentType) + || BIG_DECIMAL.equals(argumentType) || isJson) { + // no quotes required + sb.append(finalDefaultValue); + } else { + sb.append(QUOTE) + .append(finalDefaultValue) + .append(QUOTE); + } + return sb.toString(); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/FormattingHelper.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/FormattingHelper.java new file mode 100644 index 00000000000..157f1fc7878 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/FormattingHelper.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Locale; +import java.util.logging.Logger; + +import javax.json.bind.annotation.JsonbDateFormat; +import javax.json.bind.annotation.JsonbNumberFormat; + +import org.eclipse.microprofile.graphql.DateFormat; + +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BYTE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DEFAULT_LOCALE; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DOUBLE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DOUBLE_PRIMITIVE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT_PRIMITIVE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INTEGER_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INTEGER_PRIMITIVE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_DATE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_DATE_TIME_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_TIME_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LONG_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LONG_PRIMITIVE_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.OFFSET_DATE_TIME_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.OFFSET_TIME_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.SHORT_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.SUPPORTED_SCALARS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ZONED_DATE_TIME_CLASS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureRuntimeException; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getAnnotationValue; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getParameterAnnotations; + +/** + * Helper class for number formatting. + */ +class FormattingHelper { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(FormattingHelper.class.getName()); + + /** + * Defines no default format. + */ + private static final String[] NO_DEFAULT_FORMAT = new String[0]; + + /** + * Indicates date formatting applied. + */ + protected static final String DATE = "Date"; + + /** + * Indicates number formatting applied. + */ + protected static final String NUMBER = "Number"; + + /** + * Indicates no formatting applied. + */ + static final String[] NO_FORMATTING = new String[] {null, null, null }; + + /** + * No-args constructor. + */ + private FormattingHelper() { + } + + /** + * Return the default date/time format for the given class. + * + * @param scalarName scalar to check + * @param clazzName class name to validate against + * @return he default date/time format for the given class + */ + protected static String[] getDefaultDateTimeFormat(String scalarName, String clazzName) { + for (SchemaScalar scalar : SUPPORTED_SCALARS.values()) { + if (scalarName.equals(scalar.name()) && scalar.actualClass().equals(clazzName)) { + return new String[] {scalar.defaultFormat(), DEFAULT_LOCALE }; + } + } + return NO_DEFAULT_FORMAT; + } + + /** + * Return a {@link NumberFormat} for the given type, locale and format. + * + * @param type the GraphQL type or scalar + * @param locale the locale, either "" or the correct locale + * @param format the format to use, may be null + * @return The correct {@link NumberFormat} for the given type and locale + */ + protected static NumberFormat getCorrectNumberFormat(String type, String locale, String format) { + Locale actualLocale = DEFAULT_LOCALE.equals(locale) + ? Locale.getDefault() + : Locale.forLanguageTag(locale); + NumberFormat numberFormat; + if (FLOAT.equals(type) + || BIG_DECIMAL.equals(type) + || BIG_DECIMAL_CLASS.equals(type) + || FLOAT_CLASS.equals(type) + || FLOAT_PRIMITIVE_CLASS.equals(type) + || DOUBLE_CLASS.equals(type) + || DOUBLE_PRIMITIVE_CLASS.equals(type)) { + numberFormat = NumberFormat.getNumberInstance(actualLocale); + } else if (INT.equals(type) + || INTEGER_CLASS.equals(type) + || INTEGER_PRIMITIVE_CLASS.equals(type) + || BIG_INTEGER_CLASS.equals(type) + || BYTE_CLASS.equals(type) + || SHORT_CLASS.equals(type) + || LONG_CLASS.equals(type) + || LONG_PRIMITIVE_CLASS.equals(type) + || BIG_INTEGER.equals(type)) { + numberFormat = NumberFormat.getIntegerInstance(actualLocale); + } else { + return null; + } + if (format != null && !format.trim().equals("")) { + ((DecimalFormat) numberFormat).applyPattern(format); + } + return numberFormat; + } + + /** + * Return a {@link DateTimeFormatter} for the given type, locale and format. + * + * @param type the GraphQL type or scalar + * @param locale the locale, either "" or the correct locale + * @param format the format to use, may be null + * @return The correct {@link java.text.DateFormat } for the given type and locale + */ + protected static DateTimeFormatter getCorrectDateFormatter(String type, String locale, String format) { + Locale actualLocale = DEFAULT_LOCALE.equals(locale) + ? Locale.getDefault() + : Locale.forLanguageTag(locale); + + DateTimeFormatter formatter; + + if (format != null) { + formatter = DateTimeFormatter.ofPattern(format, actualLocale); + } else { + // handle defaults if no format specified + if (OFFSET_TIME_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_OFFSET_TIME.withLocale(actualLocale); + } else if (LOCAL_TIME_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_LOCAL_TIME.withLocale(actualLocale); + } else if (OFFSET_DATE_TIME_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(actualLocale); + } else if (ZONED_DATE_TIME_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale(actualLocale); + } else if (LOCAL_DATE_TIME_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(actualLocale); + } else if (LOCAL_DATE_CLASS.equals(type)) { + formatter = DateTimeFormatter.ISO_LOCAL_DATE.withLocale(actualLocale); + } else { + return null; + } + } + return formatter; + } + + /** + * Return a {@link NumberFormat} for the given type and locale. + * + * @param type the GraphQL type or scalar + * @param locale the locale, either "" or the correct locale + * @return The correct {@link NumberFormat} for the given type and locale + */ + protected static NumberFormat getCorrectNumberFormat(String type, String locale) { + return getCorrectNumberFormat(type, locale, null); + } + + /** + * Return formatting annotation details for the {@link AnnotatedElement}. The array returned contains three elements: [0] = + * either DATE, NUMBER or null if no formatting applied: [1][2] = if formatting applied then the format and locale. + * + * @param annotatedElement the {@link AnnotatedElement} to check + * @return formatting annotation details or array with three nulls if no formatting + */ + protected static String[] getFormattingAnnotation(AnnotatedElement annotatedElement) { + String[] dateFormat = getDateFormatAnnotation(annotatedElement); + String[] numberFormat = getNumberFormatAnnotation(annotatedElement); + + if (dateFormat.length == 0 && numberFormat.length == 0) { + return NO_FORMATTING; + } + if (dateFormat.length == 2 && numberFormat.length == 2) { + ensureRuntimeException(LOGGER, "A date format and number format cannot be applied to the same element: " + + annotatedElement); + } + + return dateFormat.length == 2 + ? new String[] {DATE, dateFormat[0], dateFormat[1] } + : new String[] {NUMBER, numberFormat[0], numberFormat[1] }; + } + + /** + * Return the field format with the given index. + * + * @param field {@link Field} to introspect + * @param index index of generic type. 0 = List/Collection, 1 = Map + * @return the field format with the given index + */ + protected static String[] getFieldFormat(Field field, int index) { + Annotation[] annotations = getFieldAnnotations(field, index); + if (annotations == null || annotations.length == 0) { + // try to get standard parameter annotation. + annotations = field.getAnnotatedType().getAnnotations(); + } + + return getFormatFromAnnotations(annotations); + } + + /** + * Return the method format with the given index. + * + * @param method {@link Method} to introspect + * @param index index of generic type. 0 = List/Collection, 1 = Map + * @return the method format with the given index + */ + protected static String[] getMethodFormat(Method method, int index) { + Annotation[] annotations = getMethodAnnotations(method, index); + if (annotations == null || annotations.length == 0) { + // try to get standard parameter annotation. + annotations = method.getAnnotatedReturnType().getAnnotations(); + } + return getFormatFromAnnotations(annotations); + } + + /** + * Return the method parameter format with the given index. + * + * @param parameter {@link Parameter} to introspect + * @param index index of generic type. 0 = List/Collection, 1 = Map + * @return the method parameter format with the given index + */ + protected static String[] getMethodParameterFormat(Parameter parameter, int index) { + Annotation[] annotations = getParameterAnnotations(parameter, index); + if (annotations == null || annotations.length == 0) { + // try to get standard parameter annotation. + annotations = parameter.getAnnotatedType().getAnnotations(); + } + + return getFormatFromAnnotations(annotations); + } + + /** + * Return the formatting from the given annotations. + * + * @param annotations {@link Annotation}s to retrieve formatting from + * @return the formatting from the given annotations + */ + protected static String[] getFormatFromAnnotations(Annotation[] annotations) { + if (annotations != null && annotations.length > 0) { + JsonbDateFormat dateFormat1 = (JsonbDateFormat) getAnnotationValue(annotations, JsonbDateFormat.class); + DateFormat dateFormat2 = (DateFormat) getAnnotationValue(annotations, DateFormat.class); + JsonbNumberFormat numberFormat1 = (JsonbNumberFormat) getAnnotationValue(annotations, JsonbNumberFormat.class); + org.eclipse.microprofile.graphql.NumberFormat numberFormat2 = + (org.eclipse.microprofile.graphql.NumberFormat) + getAnnotationValue(annotations, org.eclipse.microprofile.graphql.NumberFormat.class); + + // ensure that both date and number formatting are not present + if ((dateFormat1 != null || dateFormat2 != null) + && (numberFormat1 != null || numberFormat2 != null)) { + ensureRuntimeException(LOGGER, "Cannot have date and number formatting on the same method"); + } + String format = null; + String locale = null; + if (dateFormat1 != null) { + format = dateFormat1.value(); + locale = dateFormat1.locale(); + } else if (dateFormat2 != null) { + format = dateFormat2.value(); + locale = dateFormat2.locale(); + } else if (numberFormat1 != null) { + format = numberFormat1.value(); + locale = numberFormat1.locale(); + } else if (numberFormat2 != null) { + format = numberFormat2.value(); + locale = numberFormat2.locale(); + } + + if (format == null) { + return NO_FORMATTING; + } + + String type = dateFormat1 != null || dateFormat2 != null ? DATE + : NUMBER; + + return new String[] {type, format, locale.equals("") ? DEFAULT_LOCALE : locale }; + + } + return NO_FORMATTING; + } + + /** + * Indicates if either {@link JsonbNumberFormat} or {@link JsonbDateFormat} are present. + * + * @param annotatedElement the {@link AnnotatedElement} to check + * @return true if either {@link JsonbNumberFormat} or {@link JsonbDateFormat} are present + */ + protected static boolean isJsonbAnnotationPresent(AnnotatedElement annotatedElement) { + return annotatedElement.getAnnotation(JsonbDateFormat.class) != null + || annotatedElement.getAnnotation(JsonbNumberFormat.class) != null; + } + + /** + * Return the number format and locale for an {@link AnnotatedElement} if they exist in a {@link String} array. + * + * @param annotatedElement the {@link AnnotatedElement} to check + * @return the format ([0]) and locale ([1]) for a parameter in a {@link String} array or an empty array if not + */ + protected static String[] getNumberFormatAnnotation(AnnotatedElement annotatedElement) { + return getNumberFormatAnnotationInternal( + annotatedElement.getAnnotation(JsonbNumberFormat.class), + annotatedElement.getAnnotation(org.eclipse.microprofile.graphql.NumberFormat.class)); + } + + /** + * Return the number format and locale for the given annotations. + * + * @param jsonbNumberFormat {@link JsonbNumberFormat} annotation, may be null + * @param numberFormat {@link NumberFormat} annotation, may be none + * @return the format ([0]) and locale ([1]) for a method in a {@link String} array or an empty array if not + */ + private static String[] getNumberFormatAnnotationInternal(JsonbNumberFormat jsonbNumberFormat, + org.eclipse.microprofile.graphql.NumberFormat numberFormat) { + // check @NumberFormat first as this takes precedence + if (numberFormat != null) { + return new String[] {numberFormat.value(), numberFormat.locale() }; + } + if (jsonbNumberFormat != null) { + return new String[] {jsonbNumberFormat.value(), jsonbNumberFormat.locale() }; + } + return new String[0]; + } + + /** + * Return the date format and locale for a field if they exist in a {@link String} array. + * + * @param annotatedElement the {@link AnnotatedElement} to check + * @return the format ([0]) and locale ([1]) for a field in a {@link String} array or an empty array if not + */ + protected static String[] getDateFormatAnnotation(AnnotatedElement annotatedElement) { + return getDateFormatAnnotationInternal( + annotatedElement.getAnnotation(JsonbDateFormat.class), + annotatedElement.getAnnotation(org.eclipse.microprofile.graphql.DateFormat.class)); + } + + /** + * Return the date format and locale for the given annotations. + * + * @param jsonbDateFormat {@link JsonbDateFormat} annotation, may be null + * @param dateFormat {@link DateFormat} annotation, may be none + * @return the format ([0]) and locale ([1]) for a method in a {@link String} array or an empty array if not + */ + private static String[] getDateFormatAnnotationInternal(JsonbDateFormat jsonbDateFormat, + DateFormat dateFormat) { + // check @DateFormat first as this takes precedence + if (dateFormat != null) { + return new String[] {dateFormat.value(), dateFormat.locale() }; + } + if (jsonbDateFormat != null) { + return new String[] {jsonbDateFormat.value(), jsonbDateFormat.locale() }; + } + return new String[0]; + } + + /** + * Format a date value given a {@link DateTimeFormatter}. + * + * @param originalResult original result + * @param dateTimeFormatter {@link DateTimeFormatter} to format with + * @return formatted value + */ + @SuppressWarnings({"unchecked", "rawtypes" }) + public static Object formatDate(Object originalResult, DateTimeFormatter dateTimeFormatter) { + if (originalResult == null) { + return null; + } + if (originalResult instanceof Collection) { + Collection formattedResult = new ArrayList(); + Collection originalCollection = (Collection) originalResult; + originalCollection.forEach(e -> formattedResult.add(e instanceof TemporalAccessor ? dateTimeFormatter + .format((TemporalAccessor) e) : e) + ); + return formattedResult; + } else { + return originalResult instanceof TemporalAccessor + ? dateTimeFormatter.format((TemporalAccessor) originalResult) : originalResult; + } + } + + /** + * Format a number value with a a given {@link NumberFormat}. + * + * @param originalResult original result + * @param isScalar indicates if it is a scalar value + * @param numberFormat {@link NumberFormat} to format with + * @return formatted value + */ + @SuppressWarnings({"unchecked", "rawtypes" }) + public static Object formatNumber(Object originalResult, boolean isScalar, NumberFormat numberFormat) { + if (originalResult == null) { + return null; + } + if (originalResult instanceof Collection) { + Collection formattedResult = new ArrayList(); + Collection originalCollection = (Collection) originalResult; + originalCollection.forEach(e -> formattedResult.add(numberFormat.format(e))); + return formattedResult; + } + return numberFormat.format(originalResult); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlBean.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlBean.java new file mode 100644 index 00000000000..d8d2abd8636 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlBean.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.enterprise.context.control.ActivateRequestContext; +import javax.enterprise.inject.spi.CDI; + +class GraphQlBean { + @ActivateRequestContext + Object runGraphQl(Class clazz, Method method, Object[] arguments) + throws InvocationTargetException, IllegalAccessException { + Object instance = CDI.current().select(clazz).get(); + return method.invoke(instance, arguments); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlCdiExtension.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlCdiExtension.java new file mode 100644 index 00000000000..d529f61f01e --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlCdiExtension.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.BeforeBeanDiscovery; +import javax.enterprise.inject.spi.DeploymentException; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.ProcessManagedBean; +import javax.enterprise.inject.spi.WithAnnotations; + +import io.helidon.graphql.server.GraphQlSupport; +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.webserver.Routing; + +import graphql.schema.GraphQLSchema; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.graphql.ConfigKey; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Input; +import org.eclipse.microprofile.graphql.Interface; +import org.eclipse.microprofile.graphql.Type; + +import static javax.interceptor.Interceptor.Priority.LIBRARY_BEFORE; + +/** + * A CDI {@link Extension} to collect the classes that are of interest to Microprofile GraphQL. + */ +public class GraphQlCdiExtension implements Extension { + private static final Logger LOGGER = Logger.getLogger(GraphQlCdiExtension.class.getName()); + + /** + * The {@link List} of collected API's. + */ + private final Set> candidateApis = new HashSet<>(); + private final Set> collectedApis = new HashSet<>(); + + /** + * Collect the classes that have the following Microprofile GraphQL annotations. + * + * @param processAnnotatedType annotation types to process + */ + void collectCandidateApis(@Observes @WithAnnotations(GraphQLApi.class) ProcessAnnotatedType processAnnotatedType) { + Class javaClass = processAnnotatedType.getAnnotatedType().getJavaClass(); + this.candidateApis.add(javaClass); + if (javaClass.isInterface()) { + collectedApis.add(javaClass); + } + } + + void collectApis(@Observes @WithAnnotations({Type.class, Input.class, + Interface.class}) ProcessAnnotatedType processAnnotatedType) { + // these are directly added + this.collectedApis.add(processAnnotatedType.getAnnotatedType().getJavaClass()); + } + + void collectNonVetoed(@Observes ProcessManagedBean event) { + AnnotatedType type = event.getAnnotatedBeanClass(); + Class clazz = type.getJavaClass(); + + if (candidateApis.remove(clazz)) { + collectedApis.add(clazz); + } + } + + void addGraphQlBeans(@Observes BeforeBeanDiscovery event) { + event.addAnnotatedType(GraphQlBean.class, GraphQlBean.class.getName()) + .add(ApplicationScoped.Literal.INSTANCE); + } + + void clearCandidates(@Observes AfterBeanDiscovery event) { + candidateApis.clear(); + } + + void registerWithWebServer(@Observes @Priority(LIBRARY_BEFORE + 9) @Initialized(ApplicationScoped.class) Object event, + BeanManager bm) { + + Config config = ConfigProvider.getConfig(); + // this works for Helidon MP config + io.helidon.config.Config graphQlConfig = ((io.helidon.config.Config) config).get("graphql"); + + InvocationHandler.Builder handlerBuilder = InvocationHandler.builder() + .config(graphQlConfig) + .schema(createSchema()); + + config.getOptionalValue(ConfigKey.DEFAULT_ERROR_MESSAGE, String.class) + .ifPresent(handlerBuilder::defaultErrorMessage); + + config.getOptionalValue(ConfigKey.EXCEPTION_WHITE_LIST, String[].class) + .ifPresent(handlerBuilder::exceptionWhitelist); + + config.getOptionalValue(ConfigKey.EXCEPTION_BLACK_LIST, String[].class) + .ifPresent(handlerBuilder::exceptionBlacklist); + + GraphQlSupport graphQlSupport = GraphQlSupport.builder() + .config(graphQlConfig) + .invocationHandler(handlerBuilder) + .build(); + try { + ServerCdiExtension server = bm.getExtension(ServerCdiExtension.class); + Optional routingNameConfig = config.getOptionalValue("graphql.routing", String.class); + + Routing.Builder routing = routingNameConfig.stream() + .filter(Predicate.not("@default"::equals)) + .map(server::serverNamedRoutingBuilder) + .findFirst() + .orElseGet(server::serverRoutingBuilder); + + graphQlSupport.update(routing); + } catch (Throwable e) { + LOGGER.log(Level.WARNING, "Failed to set up routing with web server, maybe server extension missing?", e); + } + } + + Set> collectedApis() { + return collectedApis; + } + + private GraphQLSchema createSchema() { + try { + return SchemaGenerator.builder() + .classes(collectedApis) + .build() + .generateSchema() + .generateGraphQLSchema(); + } catch (Exception e) { + throw new DeploymentException("Failed to set up graphQL", e); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlConfigurationException.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlConfigurationException.java new file mode 100644 index 00000000000..f29613dd46c --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/GraphQlConfigurationException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +/** + * Defines an exception that is critical enough that + * will cause the GraphQL application to not start. + */ +public class GraphQlConfigurationException extends RuntimeException { + /** + * Construct a {@link GraphQlConfigurationException}. + */ + public GraphQlConfigurationException() { + super(); + } + + /** + * Construct a {@link GraphQlConfigurationException} with a given message. + * @param message exception message + */ + public GraphQlConfigurationException(String message) { + super(message); + } + + /** + * Construct a {@link GraphQlConfigurationException} with a given message and {@link Throwable}. + * @param message exception message + * @param throwable {@link Throwable} + */ + public GraphQlConfigurationException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JandexUtils.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JandexUtils.java new file mode 100644 index 00000000000..d3352b45650 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JandexUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexReader; + +/** + * Utilities for working with Jandex indexes. + */ +class JandexUtils { + + private static final Logger LOGGER = Logger.getLogger(JandexUtils.class.getName()); + + /** + * Default Jandex index file. + */ + protected static final String DEFAULT_INDEX_FILE = "META-INF/jandex.idx"; + + /** + * Property to override the default index file. (Normally used for functional tests) + */ + public static final String PROP_INDEX_FILE = "io.helidon.microprofile.graphql.indexfile"; + + /** + * The {@link Set} of loaded indexes. + */ + private Set setIndexes = new HashSet<>(); + + /** + * The file used to load the index. + */ + private String indexFile; + + /** + * Construct an instance of the utilities class.. + */ + private JandexUtils() { + indexFile = System.getProperty(PROP_INDEX_FILE, DEFAULT_INDEX_FILE); + } + + /** + * Create a new {@link JandexUtils}. + * @return a new {@link JandexUtils} + */ + public static JandexUtils create() { + return new JandexUtils(); + } + + /** + * Load all the index files of the given name. + */ + public void loadIndexes() { + try { + List listUrls = findIndexFiles(indexFile); + + // loop through each URL and load the index + for (URL url : listUrls) { + try (InputStream input = url.openStream()) { + setIndexes.add(new IndexReader(input).read()); + } catch (Exception e) { + LOGGER.warning("Unable to load default Jandex index file: " + url + + " : " + e.getMessage()); + } + } + } catch (IOException ignore) { + // any Exception coming from getResources() or toURL() is ignored and + // the Map of indexes remain empty + } + } + + /** + * Return all the Jandex index files with the given name. If the name is absolute then + * return the single file. + * + * @param indexFileName index file name + * @return a {@link List} of the index file names + * + * @throws IOException if any error + */ + private List findIndexFiles(String indexFileName) throws IOException { + List result = new ArrayList<>(); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + File file = new File(indexFile); + if (file.isAbsolute()) { + result.add(file.toPath().toUri().toURL()); + return result; + } + + Enumeration urls = contextClassLoader.getResources(indexFileName); + while (urls.hasMoreElements()) { + result.add(urls.nextElement()); + } + + return result; + } + + + /** + * Return a {@link Collection} of {@link Class}es which are implementors of a given class/interface. + * + * @param clazz {@link Class} to check for implementors + * @param includeAbstract indicates if abstract classes should be included + * @return a {@link Collection} of {@link Class}es + */ + public Collection> getKnownImplementors(String clazz, boolean includeAbstract) { + Set> setResults = new HashSet<>(); + if (!hasIndex()) { + return null; + } + + for (Index index : setIndexes) { + Set allKnownImplementors = index.getAllKnownImplementors(DotName.createSimple(clazz)); + for (ClassInfo classInfo : allKnownImplementors) { + Class clazzName = null; + try { + clazzName = Class.forName(classInfo.toString()); + } catch (ClassNotFoundException e) { + // ignore as class should exist + } + if (includeAbstract || !Modifier.isAbstract(clazzName.getModifiers())) { + setResults.add(clazzName); + } + } + } + + return setResults; + } + + /** + * Return a {@link Collection} of {@link Class}es which are implementors of a given class/interface and are not abstract. + * + * @param clazz {@link Class} to check for implementors + * @return a {@link Collection} of {@link Class}es + */ + public Collection> getKnownImplementors(String clazz) { + return getKnownImplementors(clazz, false); + } + + /** + * Indicates if an index was found. + * + * @return true if an index was found + */ + public boolean hasIndex() { + return setIndexes != null && setIndexes.size() > 0; + } + + /** + * Return the generated {@link Set} of {@link Index}es. + * + * @return the generated {@link Set} of {@link Index}es + */ + public Set getIndexes() { + return setIndexes; + } + + /** + * The index file used to load the index. (may not exist). + * + * @return index file used to load the index + */ + public String getIndexFile() { + return indexFile; + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JsonUtils.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JsonUtils.java new file mode 100644 index 00000000000..a1c6f1a7405 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/JsonUtils.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; + +import static io.helidon.microprofile.graphql.server.ElementGenerator.CLOSE_CURLY; +import static io.helidon.microprofile.graphql.server.ElementGenerator.CLOSE_SQUARE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.COLON; +import static io.helidon.microprofile.graphql.server.ElementGenerator.COMMA; +import static io.helidon.microprofile.graphql.server.ElementGenerator.COMMA_SPACE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.OPEN_CURLY; +import static io.helidon.microprofile.graphql.server.ElementGenerator.OPEN_SQUARE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.QUOTE; +import static io.helidon.microprofile.graphql.server.ElementGenerator.SPACER; +import static org.eclipse.yasson.YassonConfig.ZERO_TIME_PARSE_DEFAULTING; + +/** + * Various Json utilities. + */ +class JsonUtils { + + /** + * JSONB instance. + */ + private static final Jsonb JSONB = JsonbBuilder.newBuilder() + .withConfig(new JsonbConfig() + .setProperty(ZERO_TIME_PARSE_DEFAULTING, true) + .withNullValues(true).withAdapters()) + .build(); + + /** + * Private constructor for utilities class. + */ + private JsonUtils() { + } + + /** + * Convert a String that "should" contain JSON to a {@link Map}. + * + * @param json the Json to convert + * @return a {@link Map} containing the JSON. + */ + @SuppressWarnings("unchecked") + public static Map convertJSONtoMap(String json) { + if (json == null || json.trim().length() == 0) { + return Collections.emptyMap(); + } + return JSONB.fromJson(json, LinkedHashMap.class); + } + + /** + * Convert an {@link Map} to a Json String representation. + * + * @param map {@link Map} to convert toJson + * @return a Json String representation + */ + public static String convertMapToJson(Map map) { + return JSONB.toJson(map); + } + + /** + * Convert a Json object into the representative Java object. + * + * @param json the json + * @param clazz {@link Class} to convert to + * @return a new {@link Class} instance + */ + public static Object convertFromJson(String json, Class clazz) { + return JSONB.fromJson(json, clazz); + } + + /** + * Convert a {@link Object} to a {@link Map}. + * + * @param value {@link Object} to convert + * @return a {@link Map} representing the {@link Object} + */ + @SuppressWarnings("unchecked") + public static Map convertObjectToMap(Object value) { + return convertJSONtoMap(JSONB.toJson(value)); + } + + /** + * Convert JSON value to a GraphQLSDL like format. + * @param value value to convert + * @return JSON value converted to a GraphQLSDL format + */ + public static String convertJsonToGraphQLSDL(Object value) { + return convertJsonToGraphQLSDL(value, false); + } + + /** + * Convert JSON value to a GraphQLSDL like format. + * @param value value to convert + * @param isKey indicates if this value is a key + * @return JSON value converted to a GraphQLSDL likeformat + */ + @SuppressWarnings({"unchecked", "rawTypes"}) + public static String convertJsonToGraphQLSDL(Object value, boolean isKey) { + StringBuffer sb = new StringBuffer(); + if (value instanceof Map) { + sb.append(convertJsonMapToGraphQLSDL((Map) value)); + } else if (value instanceof Number) { + sb.append(value); + } else if (value instanceof String) { + if (isKey) { + sb.append(value.toString()); + } else { + sb.append(QUOTE).append(value.toString().replaceAll("\"", "\\\\\"")).append(QUOTE); + } + } else if (value instanceof Collection) { + sb.append(OPEN_SQUARE); + sb.append(((Collection) value).stream() + .map(JsonUtils::convertJsonToGraphQLSDL) + .collect(Collectors.joining(COMMA_SPACE))).append(CLOSE_SQUARE); + } else { + sb.append(value.toString()); + } + return sb.toString(); + } + + /** + * Convert a Json {@link Map} to a GraphQL SDL like notation. + * + * @param map Json {@link Map} to convert + * @return GraphQL SDL. + */ + @SuppressWarnings("unchecked") + private static String convertJsonMapToGraphQLSDL(Map map) { + StringBuffer sb = new StringBuffer(OPEN_CURLY); + + int count = 1; + int mapSize = map.entrySet().size(); + for (Map.Entry entry : map.entrySet()) { + if (count == 1) { + sb.append(SPACER); + } + sb.append(convertJsonToGraphQLSDL(entry.getKey(), true)).append(COLON).append(SPACER); + sb.append(convertJsonToGraphQLSDL(entry.getValue())); + if (count++ != mapSize) { + sb.append(COMMA); + } + } + + return sb.append(CLOSE_CURLY).toString(); + } +} + diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/Schema.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/Schema.java new file mode 100644 index 00000000000..965814f37e9 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/Schema.java @@ -0,0 +1,567 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import graphql.GraphQLException; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.schema.idl.TypeRuntimeWiring; + +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getSafeClass; + +/** + * The representation of a GraphQL Schema. + */ +class Schema implements ElementGenerator { + + private static final Logger LOGGER = Logger.getLogger(Schema.class.getName()); + + /** + * Default query name. + */ + public static final String QUERY = "Query"; + + /** + * Default mutation name. + */ + public static final String MUTATION = "Mutation"; + + /** + * Default subscription name. + */ + public static final String SUBSCRIPTION = "Subscription"; + + /** + * The top level query name. + */ + private String queryName; + + /** + * The top level mutation name. + */ + private String mutationName; + + /** + * The top level subscription name. + */ + private String subscriptionName; + + /** + * List of {@link SchemaType}s for this schema. This includes the standard schema types and other types. + */ + private final List listSchemaTypes; + + /** + * List of {@link SchemaScalar}s that should be included in the schema. + */ + private final List listSchemaScalars; + + /** + * List of {@link SchemaDirective}s that should be included in the schema. + */ + private final List listSchemaDirectives; + + /** + * List of {@link SchemaInputType}s that should be included in the schema. + */ + private final List listInputTypes; + + /** + * List of {@link SchemaEnum}s that should be included in the schema. + */ + private final List listSchemaEnums; + + /** + * Construct a {@link Schema}. + * + * @param builder the {@link Builder} to construct from + */ + private Schema(Builder builder) { + this.listSchemaTypes = new ArrayList<>(); + this.listSchemaScalars = new ArrayList<>(); + this.listSchemaDirectives = new ArrayList<>(); + this.listInputTypes = new ArrayList<>(); + this.listSchemaEnums = new ArrayList<>(); + this.queryName = builder.queryName; + this.subscriptionName = builder.subscriptionName; + this.mutationName = builder.mutationName; + } + + /** + * Fluent API builder to create {@link Schema}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Build a new {@link Schema}. + * + * @return a new {@link Schema} + */ + public static Schema create() { + return builder().build(); + } + + /** + * Generates a {@link GraphQLSchema} from the current discovered schema. + * + * @return {@link GraphQLSchema} + */ + public GraphQLSchema generateGraphQLSchema() { + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry; + + try { + typeDefinitionRegistry = schemaParser.parse(getSchemaAsString()); + return new graphql.schema.idl.SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, getRuntimeWiring()); + } catch (Exception e) { + String message = "Unable to parse the generated schema"; + LOGGER.warning(message + "\n" + getSchemaAsString()); + throw new GraphQLException(message, e); + } + } + + /** + * Return the GraphQL schema representation of the element. + * + * @return the GraphQL schema representation of the element. + */ + @Override + public String getSchemaAsString() { + StringBuilder sb = new StringBuilder(); + + listSchemaDirectives.forEach(d -> sb.append(d.getSchemaAsString()).append('\n')); + if (listSchemaDirectives.size() > 0) { + sb.append('\n'); + } + + sb.append("schema ").append(OPEN_CURLY).append(NEWLINE); + + // only output "query", "mutation" and "subscription" types if the + // type has at least one field definition + SchemaType queryType = getTypeByName(queryName); + SchemaType mutationType = getTypeByName(mutationName); + SchemaType subscriptionType = getTypeByName(subscriptionName); + + if (queryType != null && queryType.hasFieldDefinitions()) { + sb.append(SPACER).append("query: ").append(queryName).append('\n'); + } + if (mutationType != null && mutationType.hasFieldDefinitions()) { + sb.append(SPACER).append("mutation: ").append(mutationName).append('\n'); + } + if (subscriptionType != null && subscriptionType.hasFieldDefinitions()) { + sb.append(SPACER).append("subscription: ").append(subscriptionName).append('\n'); + } + + sb.append(CLOSE_CURLY).append(NEWLINE).append(NEWLINE); + + listSchemaTypes.forEach(t -> sb.append(t.getSchemaAsString()).append("\n")); + + listInputTypes.forEach(s -> sb.append(s.getSchemaAsString()).append('\n')); + + listSchemaEnums.forEach(e -> sb.append(e.getSchemaAsString()).append('\n')); + + listSchemaScalars.forEach(s -> sb.append(s.getSchemaAsString()).append('\n')); + + return sb.toString(); + } + + /** + * Return the {@link RuntimeWiring} for the given auto-generated schema. + * + * @return the {@link RuntimeWiring} + */ + @SuppressWarnings("checkstyle:RegexpSinglelineJava") + public RuntimeWiring getRuntimeWiring() { + RuntimeWiring.Builder builder = newRuntimeWiring(); + + // Create the top level Query Runtime Wiring. + SchemaType querySchemaType = getTypeByName(getQueryName()); + + if (querySchemaType == null) { + throw new GraphQLException("No type exists for query of name " + getQueryName()); + } + + final TypeRuntimeWiring.Builder typeRuntimeBuilder = newTypeWiring(getQueryName()); + + // register a type resolver for any interfaces if we have at least one + Set setInterfaces = getTypes().stream().filter(SchemaType::isInterface).collect(Collectors.toSet()); + if (setInterfaces.size() > 0) { + final Map mapTypes = new HashMap<>(); + + getTypes().stream().filter(t -> !t.isInterface()).forEach(t -> mapTypes.put(t.name(), t.valueClassName())); + + // generate a TypeResolver for all types that are not interfaces + TypeResolver typeResolver = env -> { + Object o = env.getObject(); + for (Map.Entry entry : mapTypes.entrySet()) { + String valueClass = entry.getValue(); + if (valueClass != null) { + Class typeClass = getSafeClass(entry.getValue()); + if (typeClass != null && typeClass.isAssignableFrom(o.getClass())) { + return (GraphQLObjectType) env.getSchema().getType(entry.getKey()); + } + } + } + return null; + }; + + // add the type resolver to all interfaces and the Query object + setInterfaces.forEach(t -> builder.type(t.name(), tr -> tr.typeResolver(typeResolver))); + builder.type(getQueryName(), tr -> tr.typeResolver(typeResolver)); + } + + // register the scalars + getScalars().forEach(s -> { + LOGGER.finest("Register Scalar: " + s); + builder.scalar(s.graphQLScalarType()); + }); + + // we should now have the query runtime binding + builder.type(typeRuntimeBuilder); + + // search for any types that have field definitions with DataFetchers + getTypes().forEach(t -> { + boolean hasDataFetchers = t.fieldDefinitions().stream().anyMatch(fd -> fd.dataFetcher() != null); + if (hasDataFetchers) { + final TypeRuntimeWiring.Builder runtimeBuilder = newTypeWiring(t.name()); + t.fieldDefinitions().stream() + .filter(fd -> fd.dataFetcher() != null) + .forEach(fd -> runtimeBuilder.dataFetcher(fd.name(), fd.dataFetcher())); + builder.type(runtimeBuilder); + } + }); + + return builder.build(); + } + + /** + * Return a {@link SchemaType} that matches the type name. + * + * @param typeName type name to match + * @return a {@link SchemaType} that matches the type name or null if none found + */ + public SchemaType getTypeByName(String typeName) { + for (SchemaType schemaType : listSchemaTypes) { + if (schemaType.name().equals(typeName)) { + return schemaType; + } + } + return null; + } + + /** + * Return a {@link SchemaInputType} that matches the type name. + * + * @param inputTypeName type name to match + * @return a {@link SchemaInputType} that matches the type name or null if none found + */ + public SchemaInputType getInputTypeByName(String inputTypeName) { + for (SchemaInputType schemaInputType : listInputTypes) { + if (schemaInputType.name().equals(inputTypeName)) { + return schemaInputType; + } + } + return null; + } + + /** + * Return a {@link SchemaType} that matches the given class. + * + * @param clazz the class to find + * @return a {@link SchemaType} that matches the given class or null if none found + */ + public SchemaType getTypeByClass(String clazz) { + for (SchemaType schemaType : listSchemaTypes) { + if (clazz.equals(schemaType.valueClassName())) { + return schemaType; + } + } + return null; + } + + /** + * Return a {@link SchemaEnum} that matches the enum name. + * + * @param enumName type name to match + * @return a {@link SchemaEnum} that matches the enum name or null if none found + */ + public SchemaEnum getEnumByName(String enumName) { + for (SchemaEnum schemaEnum1 : listSchemaEnums) { + if (schemaEnum1.name().equals(enumName)) { + return schemaEnum1; + } + } + return null; + } + + /** + * Return a {@link SchemaScalar} which matches the provided class name. + * + * @param actualClazz the class name to match + * @return {@link SchemaScalar} or null if none found + */ + public SchemaScalar getScalarByActualClass(String actualClazz) { + for (SchemaScalar schemaScalar : getScalars()) { + if (schemaScalar.actualClass().equals(actualClazz)) { + return schemaScalar; + } + } + return null; + } + + /** + * Return a {@link SchemaScalar} which matches the provided scalar name. + * + * @param scalarName the scalar name to match + * @return {@link SchemaScalar} or null if none found + */ + public SchemaScalar getScalarByName(String scalarName) { + for (SchemaScalar schemaScalar : getScalars()) { + if (schemaScalar.name().equals(scalarName)) { + return schemaScalar; + } + } + return null; + } + + /** + * Return true of the {@link SchemaType} with the the given name is present for this {@link Schema}. + * + * @param type type name to search for + * @return true if the type name is contained within the type list + */ + public boolean containsTypeWithName(String type) { + return listSchemaTypes.stream().anyMatch(t -> t.name().equals(type)); + } + + /** + * Return true of the {@link SchemaInputType} with the the given name is present for this {@link Schema}. + * + * @param type type name to search for + * @return true if the type name is contained within the input type list + */ + public boolean containsInputTypeWithName(String type) { + return listInputTypes.stream().anyMatch(t -> t.name().equals(type)); + } + + /** + * Return true of the {@link SchemaScalar} with the the given name is present for this {@link Schema}. + * + * @param scalar the scalar name to search for + * @return true if the scalar name is contained within the scalar list + */ + public boolean containsScalarWithName(String scalar) { + return listSchemaScalars.stream().anyMatch(s -> s.name().equals(scalar)); + } + + /** + * Return true of the {@link SchemaEnum} with the the given name is present for this {@link Schema}. + * + * @param enumName the enum name to search for + * @return true if the enum name is contained within the enum list + */ + public boolean containsEnumWithName(String enumName) { + return listSchemaEnums.stream().anyMatch(e -> e.name().equals(enumName)); + } + + /** + * Add a new {@link SchemaType}. + * + * @param schemaType new {@link SchemaType} to add + */ + public void addType(SchemaType schemaType) { + listSchemaTypes.add(schemaType); + } + + /** + * Add a new {@link SchemaScalar}. + * + * @param schemaScalar new {@link SchemaScalar} to add. + */ + public void addScalar(SchemaScalar schemaScalar) { + listSchemaScalars.add(schemaScalar); + } + + /** + * Add a new {@link SchemaDirective}. + * + * @param schemaDirective new {@link SchemaDirective} to add + */ + public void addDirective(SchemaDirective schemaDirective) { + listSchemaDirectives.add(schemaDirective); + } + + /** + * Add a new {@link SchemaInputType}. + * + * @param inputType new {@link SchemaInputType} to add + */ + public void addInputType(SchemaInputType inputType) { + listInputTypes.add(inputType); + } + + /** + * Add a new {@link SchemaEnum}. + * + * @param schemaEnumToAdd new {@link SchemaEnum} to add + */ + public void addEnum(SchemaEnum schemaEnumToAdd) { + listSchemaEnums.add(schemaEnumToAdd); + } + + /** + * Return the {@link List} of {@link SchemaType}s. + * + * @return the {@link List} of {@link SchemaType}s + */ + public List getTypes() { + return listSchemaTypes; + } + + /** + * Return the {@link List} of {@link SchemaScalar}s. + * + * @return the {@link List} of {@link SchemaScalar}s + */ + public List getScalars() { + return listSchemaScalars; + } + + /** + * Return the {@link List} of {@link SchemaDirective}s. + * + * @return the {@link List} of {@link SchemaDirective}s + */ + public List getDirectives() { + return listSchemaDirectives; + } + + /** + * Return the {@link List} of {@link SchemaInputType}s. + * + * @return the {@link List} of {@link SchemaInputType}s + */ + public List getInputTypes() { + return listInputTypes; + } + + /** + * Return the {@link List} of {@link SchemaEnum}s. + * + * @return the {@link List} of {@link SchemaEnum}s + */ + public List getEnums() { + return listSchemaEnums; + } + + /** + * Return the query name. + * + * @return the query name + */ + public String getQueryName() { + return queryName; + } + + /** + * Return the mutation name. + * + * @return the mutation name. + */ + public String getMutationName() { + return mutationName; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link Schema}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String queryName; + private String mutationName; + private String subscriptionName; + + private Builder() { + } + + /** + * Set the name for the query type. + * + * @param queryName name for the query type + * @return updated builder instance + */ + public Builder queryName(String queryName) { + this.queryName = queryName; + return this; + } + + /** + * Set the name for the mutation type. + * + * @param mutationName name for the query type + * @return updated builder instance + */ + public Builder mutationName(String mutationName) { + this.mutationName = mutationName; + return this; + } + + /** + * Set the name for the subscription type. + * + * @param subscriptionName name for the query type + * @return updated builder instance + */ + public Builder subscriptionName(String subscriptionName) { + this.subscriptionName = subscriptionName; + return this; + } + + @Override + public Schema build() { + if (this.queryName == null) { + this.queryName = QUERY; + } + if (this.mutationName == null) { + this.mutationName = MUTATION; + } + if (this.subscriptionName == null) { + this.subscriptionName = SUBSCRIPTION; + } + return new Schema(this); + } + } + +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaArgument.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaArgument.java new file mode 100644 index 00000000000..0f7cb1eafb1 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaArgument.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.Arrays; +import java.util.Objects; + +/** + * The representation of a GraphQL Argument or Parameter. + */ +class SchemaArgument extends AbstractDescriptiveElement implements ElementGenerator { + /** + * Argument name. + */ + private final String argumentName; + + /** + * Argument type. + */ + private String argumentType; + + /** + * Indicates if the argument is mandatory. + */ + private final boolean isMandatory; + + /** + * The default value for this argument. + */ + private Object defaultValue; + + /** + * Original argument type before it was converted to a GraphQL representation. + */ + private final Class originalType; + + /** + * Indicates if this argument is a source argument, which should be excluded from the query parameters. + */ + private boolean sourceArgument; + + /** + * Defines the format for a number or date. + */ + private String[] format; + + /** + * Indicates if the return type is an array type such as a native array([]) or a List, Collection, etc. + */ + private boolean isArrayReturnType; + + /** + * The number of array levels if return type is an array. + */ + private int arrayLevels; + + /** + * Indicates if the return type is mandatory. + */ + private boolean isArrayReturnTypeMandatory; + + /** + * Original array inner type if it is array type. + */ + private Class originalArrayType; + + /** + * Construct a {@link SchemaArgument}. + * + * @param builder the {@link Builder} to construct from + */ + private SchemaArgument(Builder builder) { + this.argumentName = builder.argumentName; + this.argumentType = builder.argumentType; + this.isMandatory = builder.isMandatory; + this.defaultValue = builder.defaultValue; + this.originalType = builder.originalType; + this.sourceArgument = builder.sourceArgument; + this.format = builder.format; + this.isArrayReturnType = builder.isArrayReturnType; + this.arrayLevels = builder.arrayLevels; + this.isArrayReturnTypeMandatory = builder.isArrayReturnTypeMandatory; + this.originalArrayType = builder.originalArrayType; + description(builder.description); + } + + /** + * Fluent API builder to create {@link SchemaArgument}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the GraphQL schema representation of the element. + * + * @return the GraphQL schema representation of the element. + */ + @Override + public String getSchemaAsString() { + StringBuilder sb = new StringBuilder(getSchemaElementDescription(format())) + .append(argumentName()) + .append(COLON); + + if (isArrayReturnType()) { + int count = arrayLevels(); + sb.append(SPACER).append(repeat(count, OPEN_SQUARE)) + .append(argumentType()) + .append(isArrayReturnTypeMandatory() ? MANDATORY : NOTHING) + .append(repeat(count, CLOSE_SQUARE)); + } else { + sb.append(SPACER).append(argumentType()); + } + + if (isMandatory) { + sb.append(MANDATORY); + } + + if (defaultValue != null) { + sb.append(generateDefaultValue(defaultValue, argumentType())); + } + + return sb.toString(); + } + + /** + * Return the argument name. + * + * @return the argument name + */ + public String argumentName() { + return argumentName; + } + + /** + * Return the argument type. + * + * @return the argument type + */ + public String argumentType() { + return argumentType; + } + + /** + * Indicates if the argument is mandatory. + * + * @return indicates if the argument is mandatory + */ + public boolean mandatory() { + return isMandatory; + } + + /** + * Set the type of the argument. + * + * @param argumentType the type of the argument + */ + public void argumentType(String argumentType) { + this.argumentType = argumentType; + } + + /** + * Return the default value for this argument. + * + * @return the default value for this argument + */ + public Object defaultValue() { + return defaultValue; + } + + /** + * Set the default value for this argument. + * + * @param defaultValue the default value for this argument + */ + public void defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Retrieve the original argument type. + * + * @return the original argument type + */ + public Class originalType() { + return originalType; + } + + /** + * Indicates if the argument is a source argument. + * + * @return if the argument is a source argument. + */ + public boolean isSourceArgument() { + return sourceArgument; + } + + /** + * Set if the argument is a source argument. + * + * @param sourceArgument if the argument is a source argument. + */ + public void sourceArgument(boolean sourceArgument) { + this.sourceArgument = sourceArgument; + } + + /** + * Return the format for a number or date. + * + * @return the format for a number or date + */ + public String[] format() { + if (format == null) { + return null; + } + String[] copy = new String[format.length]; + System.arraycopy(format, 0, copy, 0, copy.length); + return copy; + } + + /** + * Set the format for a number or date. + * + * @param format the format for a number or date + */ + public void format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + } + + /** + * Set the number of array levels if return type is an array. + * + * @param arrayLevels the number of array levels if return type is an array + */ + public void arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + } + + /** + * Return the number of array levels if return type is an array. + * + * @return the number of array levels if return type is an array + */ + public int arrayLevels() { + return arrayLevels; + } + + /** + * Set if the return type is an array type. + * @param isArrayReturnType if the return type is an array type + */ + public void arrayReturnType(boolean isArrayReturnType) { + this.isArrayReturnType = isArrayReturnType; + } + + /** + * Indicates if the return type is an array type. + * + * @return if the return type is an array type + */ + public boolean isArrayReturnType() { + return isArrayReturnType; + } + + /** + * Indicates if the array return type is mandatory. + * @return if the array return type is mandatory + */ + public boolean isArrayReturnTypeMandatory() { + return isArrayReturnTypeMandatory; + } + + /** + * Sets if the array return type is mandatory. + * @param arrayReturnTypeMandatory if the array return type is mandatory + */ + public void arrayReturnTypeMandatory(boolean arrayReturnTypeMandatory) { + isArrayReturnTypeMandatory = arrayReturnTypeMandatory; + } + + /** + * Sets the original array type. + * + * @param originalArrayType the original array type + */ + public void originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + } + + /** + * Return the original array type. + * + * @return the original array type + */ + public Class originalArrayType() { + return originalArrayType; + } + + @Override + public String toString() { + return "Argument{" + + "argumentName='" + argumentName + '\'' + + ", argumentType='" + argumentType + '\'' + + ", isMandatory=" + isMandatory + + ", defaultValue=" + defaultValue + + ", originalType=" + originalType + + ", sourceArgument=" + sourceArgument + + ", isReturnTypeMandatory=" + isArrayReturnTypeMandatory + + ", isArrayReturnType=" + isArrayReturnType + + ", originalArrayType=" + originalArrayType + + ", arrayLevels=" + arrayLevels + + ", format=" + Arrays.toString(format) + + ", description='" + description() + '\'' + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SchemaArgument schemaArgument = (SchemaArgument) o; + return isMandatory == schemaArgument.isMandatory + && Objects.equals(argumentName, schemaArgument.argumentName) + && Objects.equals(argumentType, schemaArgument.argumentType) + && Objects.equals(originalType, schemaArgument.originalType) + && Arrays.equals(format, schemaArgument.format) + && Objects.equals(sourceArgument, schemaArgument.sourceArgument) + && Objects.equals(originalArrayType, schemaArgument.originalArrayType) + && Objects.equals(description(), schemaArgument.description()) + && Objects.equals(defaultValue, schemaArgument.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), argumentName, argumentType, sourceArgument, + isMandatory, defaultValue, description(), originalType, format, originalArrayType); + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaArgument}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String argumentName; + private String argumentType; + private String description; + private boolean isMandatory; + private Object defaultValue; + private Class originalType; + private boolean sourceArgument; + private String[] format; + private boolean isArrayReturnType; + private int arrayLevels; + private boolean isArrayReturnTypeMandatory; + private Class originalArrayType; + + /** + * Set the argument name. + * @param argumentName the argument name + * @return updated builder instance + */ + public Builder argumentName(String argumentName) { + this.argumentName = argumentName; + return this; + } + + /** + * Set the argument type. + * + * @param argumentType the argument type + * @return updated builder instance + */ + public Builder argumentType(String argumentType) { + this.argumentType = argumentType; + return this; + } + + /** + * Set if the argument is mandatory. + * + * @param isMandatory true if the argument is mandatory + * @return updated builder instance + */ + public Builder mandatory(boolean isMandatory) { + this.isMandatory = isMandatory; + return this; + } + + /** + * Set the default value for this argument. + * @param defaultValue the default value for this argument + * @return updated builder instance + */ + public Builder defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Set the description of the {@link SchemaArgument}. + * @param description the description of the {@link SchemaArgument} + * @return updated builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Set the original return type. + * + * @param originalType the original return type + * @return updated builder instance + */ + public Builder originalType(Class originalType) { + this.originalType = originalType; + return this; + } + + /** + * Set if the argument is a source argument. + * @param sourceArgument true if the argument is a source argument + * @return updated builder instance + */ + public Builder sourceArgument(boolean sourceArgument) { + this.sourceArgument = sourceArgument; + return this; + } + + /** + * Set the format for a number or date. + * @param format the format for a number or date + * @return updated builder instance + */ + public Builder format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + + return this; + } + + /** + * Set if the return type is an array type such as a native array([]) or a List, Collection. + * + * @param isArrayReturnType true if the return type is an array type + * @return updated builder instance + */ + public Builder arrayReturnType(boolean isArrayReturnType) { + this.isArrayReturnType = isArrayReturnType; + return this; + } + + /** + * Set the number of array levels if return type is an array. + * + * @param arrayLevels the number of array levels if return type is an array + * @return updated builder instance + */ + public Builder arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + return this; + } + + /** + * Set if the value of the array is mandatory. + * + * @param isArrayReturnTypeMandatory If the return type is an array then indicates if the value in the + * array is mandatory + * @return updated builder instance + */ + public Builder arrayReturnTypeMandatory(boolean isArrayReturnTypeMandatory) { + this.isArrayReturnTypeMandatory = isArrayReturnTypeMandatory; + return this; + } + + /** + * Set the original array inner type if it is array type. + * @param originalArrayType the original array inner type if it is array type + * @return updated builder instance + */ + public Builder originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + return this; + } + + /** + * Build the instance from this builder. + * + * @return instance of the built type + */ + @Override + public SchemaArgument build() { + Objects.requireNonNull(argumentName, "Argument name must be specified"); + Objects.requireNonNull(argumentType, "Argument type must be specified"); + return new SchemaArgument(this); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaDirective.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaDirective.java new file mode 100644 index 00000000000..997e3db082d --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaDirective.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +/** + * The representation of a GraphQL directive. + */ +class SchemaDirective implements ElementGenerator { + + /** + * The name of the directive. + */ + private final String name; + + /** + * The list of arguments for the directive. + */ + private final List listSchemaArguments; + + /** + * The locations the directive applies to. + */ + private final Set setLocations; + + /** + * Construct a {@link SchemaDirective}. + * + * @param builder the {@link Builder} to construct from + */ + private SchemaDirective(Builder builder) { + this.name = builder.name; + this.listSchemaArguments = builder.listSchemaArguments; + this.setLocations = builder.setLocations; + } + + /** + * Fluent API builder to create {@link SchemaDirective}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the GraphQL schema representation of the element. + * + * @return the GraphQL schema representation of the element. + */ + @Override + public String getSchemaAsString() { + StringBuilder sb = new StringBuilder("directive @" + name()); + + if (listSchemaArguments.size() > 0) { + sb.append("("); + final AtomicBoolean isFirst = new AtomicBoolean(true); + listSchemaArguments.forEach(a -> { + String delim = isFirst.getAndSet(false) ? "" : ", "; + + sb.append(delim).append(a.argumentName()).append(": ").append(a.argumentType()); + if (a.mandatory()) { + sb.append('!'); + } + }); + + sb.append(")"); + } + + sb.append(" on ") + .append(setLocations.stream().sequential().collect(Collectors.joining("|"))); + + return sb.toString(); + } + + /** + * Add an {@link SchemaArgument} to the {@link SchemaDirective}. + * + * @param schemaArgument the {@link SchemaArgument} to add + */ + public void addArgument(SchemaArgument schemaArgument) { + listSchemaArguments.add(schemaArgument); + } + + /** + * Add a location to the {@link SchemaDirective}. + * + * @param location the location to add + */ + public void addLocation(String location) { + setLocations.add(location); + } + + /** + * Return the name of the {@link SchemaDirective}. + * + * @return the name of the {@link SchemaDirective} + */ + public String name() { + return name; + } + + /** + * Return the {@link List} of {@link SchemaArgument}s. + * + * @return the {@link List} of {@link SchemaArgument}s + */ + public List arguments() { + return listSchemaArguments; + } + + /** + * Return the {@link Set} of locations. + * + * @return the {@link Set} of locations + */ + public Set locations() { + return setLocations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SchemaDirective schemaDirective = (SchemaDirective) o; + return Objects.equals(name, schemaDirective.name) + && Objects.equals(listSchemaArguments, schemaDirective.listSchemaArguments) + && Objects.equals(setLocations, schemaDirective.setLocations); + } + + @Override + public int hashCode() { + return Objects.hash(name, listSchemaArguments, setLocations); + } + + @Override + public String toString() { + return "Directive{" + + "name='" + name + '\'' + + ", listArguments=" + listSchemaArguments + + ", setLocations=" + setLocations + + '}'; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaDirective}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String name; + private List listSchemaArguments = new ArrayList<>(); + private Set setLocations = new LinkedHashSet<>(); + + /** + * Set the name. + * + * @param name the name + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Add an argument to the {@link SchemaDirective}. + * + * @param argument the argument to add to the {@link SchemaDirective} + * @return updated builder instance + */ + public Builder addArgument(SchemaArgument argument) { + listSchemaArguments.add(argument); + return this; + } + + /** + * Add a location to the {@link SchemaDirective}. + * + * @param location the location to add to the {@link SchemaDirective} + * @return updated builder instance + */ + public Builder addLocation(String location) { + this.setLocations.add(location); + return this; + } + + @Override + public SchemaDirective build() { + Objects.requireNonNull(name, "Name must be specified"); + return new SchemaDirective(this); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaEnum.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaEnum.java new file mode 100644 index 00000000000..5627530c3e2 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaEnum.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * The representation of a GraphQL Enum. + */ +class SchemaEnum extends AbstractDescriptiveElement implements ElementGenerator { + + /** + * The name of the enum. + */ + private String name; + + /** + * The values for the enum. + */ + private List values; + + /** + * Construct a {@link SchemaEnum}. + * + * @param builder the {@link SchemaDirective.Builder} to construct from + */ + private SchemaEnum(Builder builder) { + this.name = builder.name; + this.values = builder.values; + description(builder.description); + } + + /** + * Fluent API builder to create {@link SchemaEnum}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the name of the {@link SchemaEnum}. + * @return the name of the {@link SchemaEnum} + */ + public String name() { + return name; + } + + /** + * Set the name of the {@link SchemaEnum}. + * @param name the name of the {@link SchemaEnum} + */ + public void name(String name) { + this.name = name; + } + + /** + * Return the values for the {@link SchemaEnum}. + * @return the values for the {@link SchemaEnum} + */ + public List values() { + return values; + } + + /** + * Add a value to the {@link SchemaEnum}. + * @param value value to add + */ + public void addValue(String value) { + this.values.add(value); + } + + @Override + public String getSchemaAsString() { + StringBuilder sb = new StringBuilder(getSchemaElementDescription(null)) + .append("enum") + .append(SPACER) + .append(name()) + .append(SPACER) + .append(OPEN_CURLY) + .append(NEWLINE); + + values.forEach(v -> sb.append(SPACER).append(v).append(NEWLINE)); + + return sb.append(CLOSE_CURLY).append(NEWLINE).toString(); + } + + @Override + public String toString() { + return "Enum{" + + "name='" + name + '\'' + + ", values=" + values + + ", description='" + description() + '\'' + '}'; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaDirective}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String name; + private List values = new ArrayList<>(); + private String description; + + /** + * Set the name. + * + * @param name the name of the {@link SchemaEnum} + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Add a value to the {@link SchemaEnum}. + * + * @param value value to add + * @return updated builder instance + */ + public Builder addValue(String value) { + this.values.add(value); + return this; + } + + /** + * Set the description. + * @param description the description of the {@link SchemaEnum} + * @return updated builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + @Override + public SchemaEnum build() { + Objects.requireNonNull(name, "Name must be specified"); + return new SchemaEnum(this); + } + } + +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinition.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinition.java new file mode 100644 index 00000000000..099c3208ad7 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinition.java @@ -0,0 +1,635 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import graphql.schema.DataFetcher; + +/** + * The representation of a GraphQL Field Definition. + */ +class SchemaFieldDefinition extends AbstractDescriptiveElement implements ElementGenerator { + /** + * Name of the field definition. + */ + private final String name; + + /** + * Return type. + */ + private String returnType; + + /** + * Indicates if the return type is an array type such as a native array([]) or a List, Collection, etc. + */ + private final boolean isArrayReturnType; + + /** + * The number of array levels if return type is an array. + */ + private final int arrayLevels; + + /** + * Indicates if the return type is mandatory. + */ + private final boolean isReturnTypeMandatory; + + /** + * List of arguments. + */ + private final List listSchemaArguments; + + /** + * If the return type is an array then indicates if the value in the + * array is mandatory. + */ + private boolean isArrayReturnTypeMandatory; + + /** + * {@link DataFetcher} to override default behaviour of field. + */ + private DataFetcher dataFetcher; + + /** + * Original type before it was converted to a GraphQL representation. + */ + private Class originalType; + + /** + * Original array inner type if it is array type. + */ + private Class originalArrayType; + + /** + * Defines the format for a number or date. + */ + private String[] format; + + /** + * The default value for this field definition. Only valid for field definitions of an input type. + */ + private Object defaultValue; + + /** + * Indicates if the field has a default format applied (such as default date format) rather than a specific format supplied. + */ + private boolean defaultFormatApplied; + + /** + * Indicates if the format is of type Jsonb. + */ + private boolean isJsonbFormat; + + /** + * Indicates if the property name is of type Jsonb. + */ + private boolean isJsonbProperty; + + /** + * Construct a {@link SchemaFieldDefinition}. + * + * @param builder the {@link Builder} to construct from + */ + private SchemaFieldDefinition(Builder builder) { + this.name = builder.name; + this.returnType = builder.returnType; + this.isArrayReturnType = builder.isArrayReturnType; + this.arrayLevels = builder.arrayLevels; + this.isReturnTypeMandatory = builder.isReturnTypeMandatory; + this.listSchemaArguments = builder.listSchemaArguments; + this.isArrayReturnTypeMandatory = builder.isArrayReturnTypeMandatory; + this.dataFetcher = builder.dataFetcher; + this.originalType = builder.originalType; + this.originalArrayType = builder.originalArrayType; + this.format = builder.format; + this.defaultValue = builder.defaultValue; + this.defaultFormatApplied = builder.defaultFormatApplied; + this.isJsonbFormat = builder.isJsonbFormat; + this.isJsonbProperty = builder.isJsonbProperty; + description(builder.description); + } + + /** + * Fluent API builder to create {@link SchemaFieldDefinition}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public String getSchemaAsString() { + StringBuilder sb = new StringBuilder(getSchemaElementDescription(format())) + .append(name()); + + if (listSchemaArguments.size() > 0) { + sb.append(OPEN_PARENTHESES) + .append(NEWLINE) + .append(listSchemaArguments.stream() + .map(SchemaArgument::getSchemaAsString) + .collect(Collectors.joining(COMMA_SPACE + NEWLINE))); + sb.append(NEWLINE).append(CLOSE_PARENTHESES); + } + + sb.append(COLON); + + if (isArrayReturnType()) { + int count = arrayLevels(); + sb.append(SPACER).append(repeat(count, OPEN_SQUARE)) + .append(returnType()) + .append(isArrayReturnTypeMandatory() ? MANDATORY : NOTHING) + .append(repeat(count, CLOSE_SQUARE)); + } else { + sb.append(SPACER).append(returnType()); + } + + if (isReturnTypeMandatory()) { + sb.append(MANDATORY); + } + + if (defaultValue != null) { + sb.append(generateDefaultValue(defaultValue, returnType())); + } + + return sb.toString(); + } + + /** + * Return the name for the field definition. + * + * @return the name for the field definition + */ + public String name() { + return name; + } + + /** + * Return the {@link List} of {@link SchemaArgument}s. + * + * @return the {@link List} of {@link SchemaArgument}s + */ + public List arguments() { + return listSchemaArguments; + } + + /** + * Return the return type. + * + * @return the return type + */ + public String returnType() { + return returnType; + } + + /** + * Indicates if the return type is an array type. + * + * @return if the return type is an array type + */ + public boolean isArrayReturnType() { + return isArrayReturnType; + } + + /** + * Indicates if the return type is mandatory. + * + * @return if the return type is mandatory + */ + public boolean isReturnTypeMandatory() { + return isReturnTypeMandatory; + } + + /** + * Return the {@link DataFetcher} for this {@link SchemaFieldDefinition}. + * + * @return he {@link DataFetcher} for this {@link SchemaFieldDefinition} + */ + public DataFetcher dataFetcher() { + return dataFetcher; + } + + /** + * Set the return type. + * + * @param sReturnType the return type + */ + public void returnType(String sReturnType) { + returnType = sReturnType; + } + + /** + * Set the {@link DataFetcher} which will override the default {@link DataFetcher} for the field. + * + * @param dataFetcher the {@link DataFetcher} + */ + public void dataFetcher(DataFetcher dataFetcher) { + this.dataFetcher = dataFetcher; + } + + /** + * Add a {@link SchemaArgument} to this {@link SchemaFieldDefinition}. + * + * @param schemaArgument {@link SchemaArgument} to add + */ + public void addArgument(SchemaArgument schemaArgument) { + listSchemaArguments.add(schemaArgument); + } + + /** + * Return the number of array levels if return type is an array. + * + * @return the number of array levels if return type is an array + */ + public int arrayLevels() { + return arrayLevels; + } + + /** + * Return the format for a number or date. + * + * @return the format for a number or date + */ + public String[] format() { + if (format == null) { + return null; + } + String[] copy = new String[format.length]; + System.arraycopy(format, 0, copy, 0, copy.length); + return copy; + } + + /** + * Set the format for a number or date. + * + * @param format the format for a number or date + */ + public void format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + } + + /** + * Return the default value for this field definition. + * + * @return the default value for this field definition + */ + public Object defaultValue() { + return defaultValue; + } + + /** + * Set the default value for this field definition. + * + * @param defaultValue the default value for this field definition + */ + public void defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Sets the original return type. + * + * @param originalType the original return type + */ + public void originalType(Class originalType) { + this.originalType = originalType; + } + + /** + * Retrieve the original return type. + * + * @return the original return type + */ + public Class originalType() { + return originalType; + } + + /** + * Sets the original array type. + * + * @param originalArrayType the original array type + */ + public void originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + } + + /** + * Return the original array type. + * + * @return the original array type + */ + public Class originalArrayType() { + return originalArrayType; + } + + /** + * Return if the array return type is mandatory. + * @return if the array return type is mandatory + */ + public boolean isArrayReturnTypeMandatory() { + return isArrayReturnTypeMandatory; + } + + /** + * Sets if the array return type is mandatory. + * @param arrayReturnTypeMandatory if the array return type is mandatory + */ + public void arrayReturnTypeMandatory(boolean arrayReturnTypeMandatory) { + isArrayReturnTypeMandatory = arrayReturnTypeMandatory; + } + + /** + * Set if the field has a default format applied. + * @param defaultFormatApplied if the field has a default format applied + */ + public void defaultFormatApplied(boolean defaultFormatApplied) { + this.defaultFormatApplied = defaultFormatApplied; + } + + /** + * Return if the field has a default format applied. + * @return if the field has a default format applied + */ + public boolean isDefaultFormatApplied() { + return defaultFormatApplied; + } + + /** + * Set if the format is of type Jsonb. + * @param isJsonbFormat if the format is of type Jsonb + */ + public void jsonbFormat(boolean isJsonbFormat) { + this.isJsonbFormat = isJsonbFormat; + } + + /** + * Return true if the format is of type Jsonb. + * @return true if the format is of type Jsonb + */ + public boolean isJsonbFormat() { + return isJsonbFormat; + } + + /** + * Sets if the property has a JsonbProperty annotation. + * + * @param isJsonbProperty if the property has a JsonbProperty annotation + */ + public void jsonbProperty(boolean isJsonbProperty) { + this.isJsonbProperty = isJsonbProperty; + } + + /** + * Indicates if the property has a JsonbProperty annotation. + * + * @return true if the property has a JsonbProperty annotation + */ + public boolean isJsonbProperty() { + return isJsonbProperty; + } + + @Override + public String toString() { + return "FieldDefinition{" + + "name='" + name + '\'' + + ", returnType='" + returnType + '\'' + + ", isArrayReturnType=" + isArrayReturnType + + ", isReturnTypeMandatory=" + isReturnTypeMandatory + + ", isArrayReturnTypeMandatory=" + isArrayReturnTypeMandatory + + ", listArguments=" + listSchemaArguments + + ", arrayLevels=" + arrayLevels + + ", originalType=" + originalType + + ", defaultFormatApplied=" + defaultFormatApplied + + ", originalArrayType=" + originalArrayType + + ", format=" + Arrays.toString(format) + + ", isJsonbFormat=" + isJsonbFormat + + ", isJsonbProperty=" + isJsonbProperty + + ", description='" + description() + '\'' + '}'; + } + + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaFieldDefinition}. + */ + public static class Builder implements io.helidon.common.Builder { + private String name; + private String returnType; + private boolean isArrayReturnType; + private int arrayLevels; + private boolean isReturnTypeMandatory; + private List listSchemaArguments = new ArrayList<>(); + private boolean isArrayReturnTypeMandatory; + private DataFetcher dataFetcher; + private Class originalType; + private Class originalArrayType; + private String[] format; + private Object defaultValue; + private boolean defaultFormatApplied; + private boolean isJsonbFormat; + private boolean isJsonbProperty; + private String description; + + /** + * Set the name of the {@link SchemaFieldDefinition}. + * + * @param name the name of the {@link SchemaFieldDefinition} + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the returnType. + * + * @param returnType the returnType + * @return updated builder instance + */ + public Builder returnType(String returnType) { + this.returnType = returnType; + return this; + } + + /** + * Set if the return type is an array type such as a native array([]) or a List, Collection. + * + * @param isArrayReturnType true if the return type is an array type + * @return updated builder instance + */ + public Builder arrayReturnType(boolean isArrayReturnType) { + this.isArrayReturnType = isArrayReturnType; + return this; + } + + /** + * Set if the return type is mandatory. + * + * @param isReturnTypeMandatory true if the return type is mandatory. + * @return updated builder instance + */ + public Builder returnTypeMandatory(boolean isReturnTypeMandatory) { + this.isReturnTypeMandatory = isReturnTypeMandatory; + return this; + } + + /** + * Set the number of array levels if return type is an array. + * + * @param arrayLevels the number of array levels if return type is an array + * @return updated builder instance + */ + public Builder arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + return this; + } + + /** + * Add an argument to the {@link SchemaFieldDefinition}. + * + * @param argument the argument to add to the {@link SchemaFieldDefinition} + * @return updated builder instance + */ + public Builder addArgument(SchemaArgument argument) { + listSchemaArguments.add(argument); + return this; + } + + /** + * Set if the value of the array is mandatory. + * + * @param isArrayReturnTypeMandatory If the return type is an array then indicates if the value in the + * array is mandatory + * @return updated builder instance + */ + public Builder arrayReturnTypeMandatory(boolean isArrayReturnTypeMandatory) { + this.isArrayReturnTypeMandatory = isArrayReturnTypeMandatory; + return this; + } + + /** + * Set the {@link DataFetcher} to override default behaviour of field. + * @param dataFetcher {@link DataFetcher} to override default behaviour of field + * @return updated builder instance + */ + public Builder dataFetcher(DataFetcher dataFetcher) { + this.dataFetcher = dataFetcher; + return this; + } + + /** + * Set the original return type. + * @param originalType the original return type + * @return updated builder instance + */ + public Builder originalType(Class originalType) { + this.originalType = originalType; + return this; + } + + /** + * Set the original array inner type if it is array type. + * @param originalArrayType the original array inner type if it is array type + * @return updated builder instance + */ + public Builder originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + return this; + } + + /** + * Set the format for a number or date. + * @param format the format for a number or date + * @return updated builder instance + */ + public Builder format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + + return this; + } + + /** + * Set the default value for this field definition. Only valid for field definitions of an input type. + * @param defaultValue the default value for this field definition + * @return updated builder instance + */ + public Builder defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Set if the field has a default format applied. + * @param defaultFormatApplied true if the field has a default format applied + * @return updated builder instance + */ + public Builder defaultFormatApplied(boolean defaultFormatApplied) { + this.defaultFormatApplied = defaultFormatApplied; + return this; + } + + /** + * Set if the format is of type Jsonb. + * @param isJsonbFormat if the format is of type Jsonb. + * @return updated builder instance + */ + public Builder jsonbFormat(boolean isJsonbFormat) { + this.isJsonbFormat = isJsonbFormat; + return this; + } + /** + * Set if the property name is of type Jsonb. + * @param isJsonbProperty if the property name is of type Jsonb. + * @return updated builder instance + */ + public Builder jsonbProperty(boolean isJsonbProperty) { + this.isJsonbProperty = isJsonbProperty; + return this; + } + + /** + * Set the description. + * @param description the description of the {@link SchemaFieldDefinition} + * @return updated builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + @Override + public SchemaFieldDefinition build() { + Objects.requireNonNull(name, "Name must be specified"); + return new SchemaFieldDefinition(this); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGenerator.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGenerator.java new file mode 100644 index 00000000000..a5ba13e714e --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGenerator.java @@ -0,0 +1,1779 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.json.bind.annotation.JsonbProperty; + +import io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetcherFactories; +import graphql.schema.GraphQLScalarType; +import graphql.schema.PropertyDataFetcher; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Input; +import org.eclipse.microprofile.graphql.Interface; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; +import org.eclipse.microprofile.graphql.Type; + +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_OFFSET_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_ZONED_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.FormattingHelper.DATE; +import static io.helidon.microprofile.graphql.server.FormattingHelper.NO_FORMATTING; +import static io.helidon.microprofile.graphql.server.FormattingHelper.NUMBER; +import static io.helidon.microprofile.graphql.server.FormattingHelper.formatDate; +import static io.helidon.microprofile.graphql.server.FormattingHelper.formatNumber; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectDateFormatter; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectNumberFormat; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getFormattingAnnotation; +import static io.helidon.microprofile.graphql.server.FormattingHelper.isJsonbAnnotationPresent; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod.MUTATION_TYPE; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod.QUERY_TYPE; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_OFFSET_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_ZONED_DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ID; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.STRING; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.checkScalars; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureConfigurationException; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureFormat; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureValidName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getAnnotationValue; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getArrayLevels; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDefaultValueAnnotationValue; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDescription; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getGraphQLType; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getParameterAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getRootArrayClass; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getSafeClass; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getScalar; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getSimpleName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getTypeName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isArrayType; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isDateTimeScalar; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isEnumClass; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isGraphQLType; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isPrimitive; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.shouldIgnoreField; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.shouldIgnoreMethod; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.stripMethodName; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.validateIDClass; + +/** + * Various utilities for generating {@link Schema}s from classes. + */ +class SchemaGenerator { + + /** + * "is" prefix. + */ + protected static final String IS = "is"; + + /** + * "get" prefix. + */ + protected static final String GET = "get"; + + /** + * "set" prefix. + */ + protected static final String SET = "set"; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SchemaGenerator.class.getName()); + + /** + * {@link JandexUtils} instance to hold indexes. + */ + private JandexUtils jandexUtils; + + /** + * Holds the {@link Set} of unresolved types while processing the annotations. + */ + private Set setUnresolvedTypes = new HashSet<>(); + + /** + * Holds the {@link Set} of additional methods that need to be added to types. + */ + private Set setAdditionalMethods = new HashSet<>(); + + private final Set> collectedApis = new HashSet<>(); + + /** + * Construct a {@link SchemaGenerator}. + * + * @param builder the {@link io.helidon.microprofile.graphql.server.SchemaGenerator.Builder} to construct from + */ + private SchemaGenerator(Builder builder) { + this.collectedApis.addAll(builder.collectedApis); + jandexUtils = JandexUtils.create(); + jandexUtils.loadIndexes(); + if (!jandexUtils.hasIndex()) { + String message = "Unable to find or load jandex index files: " + + jandexUtils.getIndexFile() + ".\nEnsure you are using the " + + "jandex-maven-plugin when you are building your application"; + LOGGER.warning(message); + } + } + + /** + * Fluent API builder to create {@link SchemaGenerator}. + * + * @return new builder instance + */ + public static Builder builder() { + return new SchemaGenerator.Builder(); + } + + /** + * Generate a {@link Schema} by scanning all discovered classes using the {@link GraphQlCdiExtension}. + * + * @return a {@link Schema} + * @throws java.lang.IllegalStateException in case the schema cannot be generated + */ + public Schema generateSchema() { + int count = collectedApis.size(); + + LOGGER.info("Discovered " + count + " annotated GraphQL API class" + (count != 1 ? "es" : "")); + + try { + return generateSchemaFromClasses(collectedApis); + } catch (IntrospectionException | ClassNotFoundException e) { + throw new IllegalStateException("Cannot generate schema", e); + } + } + + /** + * Generate a {@link Schema} from a given array of classes. The classes are checked to see if they contain any of the + * annotations from the microprofile spec. + * + * @param clazzes array of classes to check + * @return a {@link Schema} + * + * @throws IntrospectionException if any errors with introspection + * @throws ClassNotFoundException if any classes are not found + */ + protected Schema generateSchemaFromClasses(Set> clazzes) throws IntrospectionException, ClassNotFoundException { + Schema schema = Schema.create(); + setUnresolvedTypes.clear(); + setAdditionalMethods.clear(); + + SchemaType rootQueryType = SchemaType.builder().name(schema.getQueryName()).build(); + SchemaType rootMutationType = SchemaType.builder().name(schema.getMutationName()).build(); + + // process any specific classes with the Input, Type or Interface annotations + for (Class clazz : clazzes) { + // only include interfaces and concrete classes/enums + if (clazz.isInterface() || (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()))) { + // Discover Enum via annotation + if (clazz.isAnnotationPresent(org.eclipse.microprofile.graphql.Enum.class)) { + schema.addEnum(generateEnum(clazz)); + continue; + } + + // Type, Interface, Input are all treated similarly + Type typeAnnotation = clazz.getAnnotation(Type.class); + Interface interfaceAnnotation = clazz.getAnnotation(Interface.class); + Input inputAnnotation = clazz.getAnnotation(Input.class); + + if (typeAnnotation != null && inputAnnotation != null) { + ensureConfigurationException(LOGGER, "Class " + clazz.getName() + " has been annotated with" + + " both Type and Input"); + } + + if (typeAnnotation != null || interfaceAnnotation != null) { + if (interfaceAnnotation != null && !clazz.isInterface()) { + ensureConfigurationException(LOGGER, "Class " + clazz.getName() + " has been annotated with" + + " @Interface but is not one"); + } + + // assuming value for annotation overrides @Name + String typeName = getTypeName(clazz, true); + SchemaType type = SchemaType.builder() + .name(typeName.isBlank() ? clazz.getSimpleName() : typeName) + .valueClassName(clazz.getName()).build(); + type.isInterface(clazz.isInterface()); + type.description(getDescription(clazz.getAnnotation(Description.class))); + + // add the discovered type + addTypeToSchema(schema, type); + + if (type.isInterface()) { + // is an interface so check for any implementors and add them too + jandexUtils.getKnownImplementors(clazz.getName()).forEach(c -> setUnresolvedTypes.add(c.getName())); + } + } else if (inputAnnotation != null) { + String clazzName = clazz.getName(); + String simpleName = clazz.getSimpleName(); + + SchemaInputType inputType = generateType(clazzName, true).createInputType(""); + // if the name of the InputType was not changed then append "Input" + if (inputType.name().equals(simpleName)) { + inputType.name(inputType.name() + "Input"); + } + + if (!schema.containsInputTypeWithName(inputType.name())) { + schema.addInputType(inputType); + checkInputType(schema, inputType); + } + } + + // obtain top level query API's + if (clazz.isAnnotationPresent(GraphQLApi.class)) { + processGraphQLApiAnnotations(rootQueryType, rootMutationType, schema, clazz); + } + } + } + + schema.addType(rootQueryType); + schema.addType(rootMutationType); + + // process unresolved types + processUnresolvedTypes(schema); + + // look though all of interface type and see if any of the known types implement + // the interface and if so, add the interface to the type + schema.getTypes().stream().filter(SchemaType::isInterface).forEach(it -> { + schema.getTypes().stream().filter(t -> !t.isInterface() && t.valueClassName() != null).forEach(type -> { + Class interfaceClass = getSafeClass(it.valueClassName()); + Class typeClass = getSafeClass(type.valueClassName()); + if (interfaceClass != null + && typeClass != null + && interfaceClass.isAssignableFrom(typeClass)) { + type.implementingInterface(it.name()); + } + }); + }); + + // process any additional methods required via the @Source annotation + for (DiscoveredMethod dm : setAdditionalMethods) { + // add the discovered method to the type + SchemaType type = schema.getTypeByClass(dm.source()); + if (type != null) { + SchemaFieldDefinition fd = newFieldDefinition(dm, null); + // add all arguments which are not source arguments + if (dm.arguments().size() > 0) { + dm.arguments().stream().filter(a -> !a.isSourceArgument()) + .forEach(fd::addArgument); + } + + // check for existing DataFetcher + fd.dataFetcher(DataFetcherUtils.newMethodDataFetcher( + schema, dm.method().getDeclaringClass(), dm.method(), + dm.source(), fd.arguments().toArray(new SchemaArgument[0]))); + type.addFieldDefinition(fd); + + // we are creating this as a type so ignore any Input annotation + String simpleName = getSimpleName(fd.returnType(), true); + String returnType = fd.returnType(); + if (!simpleName.equals(returnType)) { + updateLongTypes(schema, returnType, simpleName); + } + } + } + + // process default values for dates + processDefaultDateTimeValues(schema); + + // process the @GraphQLApi annotated classes + if (rootQueryType.fieldDefinitions().size() == 0 && rootMutationType.fieldDefinitions().size() == 0) { + LOGGER.warning("Unable to find any classes with @GraphQLApi annotation." + + "Unable to build schema"); + } + + return schema; + } + + /** + * Process all {@link SchemaFieldDefinition}s and {@link SchemaArgument}s and update the default values for any scalars. + * + * @param schema {@link Schema} to update + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void processDefaultDateTimeValues(Schema schema) { + // concatenate both the SchemaType and SchemaInputType + Stream streamInputTypes = schema.getInputTypes().stream().map(it -> (SchemaType) it); + Stream streamAll = Stream.concat(streamInputTypes, schema.getTypes().stream()); + streamAll.forEach(t -> { + t.fieldDefinitions().forEach(fd -> { + String returnType = fd.returnType(); + // only check Date/Time/DateTime scalars that are not Queries or don't have data fetchers + // as default formatting has already been dealt with + if (isDateTimeScalar(returnType) && (t.name().equals(Schema.QUERY) || fd.dataFetcher() == null)) { + String[] existingFormat = fd.format(); + // check if this type is an array type and if so then get the actual original type + Class clazzOriginalType = fd.originalArrayType() != null + ? fd.originalArrayType() : fd.originalType(); + String[] newFormat = ensureFormat(returnType, clazzOriginalType.getName(), existingFormat); + if (!Arrays.equals(newFormat, existingFormat) && newFormat.length == 2) { + // formats differ so set the new format and DataFetcher + fd.format(newFormat); + if (fd.dataFetcher() == null) { + // create the raw array to pass to the retrieveFormattingDataFetcher method + DataFetcher dataFetcher = retrieveFormattingDataFetcher( + new String[] {DATE, newFormat[0], newFormat[1]}, + fd.name(), clazzOriginalType.getName()); + fd.dataFetcher(dataFetcher); + } + fd.defaultFormatApplied(true); + SchemaScalar scalar = schema.getScalarByName(fd.returnType()); + GraphQLScalarType newScalarType = null; + if (fd.returnType().equals(FORMATTED_DATE_SCALAR)) { + fd.returnType(DATE_SCALAR); + newScalarType = CUSTOM_DATE_SCALAR; + } else if (fd.returnType().equals(FORMATTED_TIME_SCALAR)) { + fd.returnType(TIME_SCALAR); + newScalarType = CUSTOM_TIME_SCALAR; + } else if (fd.returnType().equals(FORMATTED_DATETIME_SCALAR)) { + fd.returnType(DATETIME_SCALAR); + newScalarType = CUSTOM_DATE_TIME_SCALAR; + } else if (fd.returnType().equals(FORMATTED_OFFSET_DATETIME_SCALAR)) { + fd.returnType(FORMATTED_OFFSET_DATETIME_SCALAR); + newScalarType = CUSTOM_OFFSET_DATE_TIME_SCALAR; + } else if (fd.returnType().equals(FORMATTED_ZONED_DATETIME_SCALAR)) { + fd.returnType(FORMATTED_ZONED_DATETIME_SCALAR); + newScalarType = CUSTOM_ZONED_DATE_TIME_SCALAR; + } + + // clone the scalar with the new scalar name + SchemaScalar newScalar = new SchemaScalar(fd.returnType(), scalar.actualClass(), + newScalarType, scalar.defaultFormat()); + if (!schema.containsScalarWithName(newScalar.name())) { + schema.addScalar(newScalar); + } + } + } + + // check the SchemaArguments + fd.arguments().forEach(a -> { + String argumentType = a.argumentType(); + if (isDateTimeScalar(argumentType)) { + String[] existingArgFormat = a.format(); + Class clazzOriginalType = a.originalArrayType() != null + ? a.originalArrayType() : a.originalType(); + String[] newArgFormat = ensureFormat(argumentType, clazzOriginalType.getName(), existingArgFormat); + if (!Arrays.equals(newArgFormat, existingArgFormat) && newArgFormat.length == 2) { + a.format(newArgFormat); + } + } + }); + }); + }); + } + + /** + * Process any unresolved types. + * + * @param schema {@link Schema} to add types to + */ + private void processUnresolvedTypes(Schema schema) { + // create any types that are still unresolved. e.g. an Order that contains OrderLine objects + // also ensure if the unresolved type contains another unresolved type then we process it + while (setUnresolvedTypes.size() > 0) { + String returnType = setUnresolvedTypes.iterator().next(); + + setUnresolvedTypes.remove(returnType); + try { + String simpleName = getSimpleName(returnType, true); + + SchemaScalar scalar = getScalar(returnType); + if (scalar != null) { + if (!schema.containsScalarWithName(scalar.name())) { + schema.addScalar(scalar); + } + // update the return type with the scalar + updateLongTypes(schema, returnType, scalar.name()); + } else if (isEnumClass(returnType)) { + SchemaEnum newEnum = generateEnum(Class.forName(returnType)); + if (!schema.containsEnumWithName(simpleName)) { + schema.addEnum(newEnum); + } + updateLongTypes(schema, returnType, newEnum.name()); + } else { + // we will either know this type already or need to add it + boolean fExists = schema.getTypes().stream() + .filter(t -> t.name().equals(simpleName)).count() > 0; + if (!fExists) { + SchemaType newType = generateType(returnType, false); + + // update any return types to the discovered scalars + checkScalars(schema, newType); + schema.addType(newType); + } + // need to update any FieldDefinitions that contained the original "long" type of c + updateLongTypes(schema, returnType, simpleName); + } + } catch (Exception e) { + ensureConfigurationException(LOGGER, "Cannot get GraphQL type for " + returnType, e); + } + } + } + + /** + * Generate a {@link SchemaType} from a given class. + * + * @param realReturnType the class to generate type from + * @param isInputType indicates if the type is an input type and if not the Input annotation will be ignored + * @return a {@link SchemaType} + * @throws IntrospectionException if any errors with introspection + * @throws ClassNotFoundException if any classes are not found + */ + private SchemaType generateType(String realReturnType, boolean isInputType) + throws IntrospectionException, ClassNotFoundException { + + // if isInputType=false then we ignore the name annotation in case + // an annotated input type was also used as a return type + String simpleName = getSimpleName(realReturnType, !isInputType); + SchemaType type = SchemaType.builder().name(simpleName).valueClassName(realReturnType).build(); + type.description(getDescription(Class.forName(realReturnType).getAnnotation(Description.class))); + + for (Map.Entry entry + : retrieveGetterBeanMethods(Class.forName(realReturnType), isInputType).entrySet()) { + DiscoveredMethod discoveredMethod = entry.getValue(); + String valueTypeName = discoveredMethod.returnType(); + SchemaFieldDefinition fd = newFieldDefinition(discoveredMethod, null); + type.addFieldDefinition(fd); + + if (!ID.equals(valueTypeName) && valueTypeName.equals(fd.returnType())) { + // value class was unchanged meaning we need to resolve + setUnresolvedTypes.add(valueTypeName); + } + } + return type; + } + + /** + * Process a class with a {@link GraphQLApi} annotation. + * + * @param rootQueryType the root query type + * @param rootMutationType the root mutation type + * @param clazz {@link Class} to introspect + * @throws IntrospectionException + */ + @SuppressWarnings("rawtypes") + private void processGraphQLApiAnnotations(SchemaType rootQueryType, + SchemaType rootMutationType, + Schema schema, + Class clazz) + throws IntrospectionException, ClassNotFoundException { + + for (Map.Entry entry + : retrieveAllAnnotatedBeanMethods(clazz).entrySet()) { + DiscoveredMethod discoveredMethod = entry.getValue(); + Method method = discoveredMethod.method(); + + SchemaFieldDefinition fd = null; + + // only include discovered methods in the original type where either the source is null + // or the source is not null and it has a query annotation + String source = discoveredMethod.source(); + if (source == null || discoveredMethod.isQueryAnnotated()) { + fd = newFieldDefinition(discoveredMethod, getMethodName(method)); + } + + // if the source was not null, save it for later processing on the correct type + if (source != null) { + String additionReturnType = getGraphQLType(discoveredMethod.returnType()); + setAdditionalMethods.add(discoveredMethod); + // add the unresolved type for the source + if (!isGraphQLType(additionReturnType)) { + setUnresolvedTypes.add(additionReturnType); + } + } + + SchemaType schemaType = discoveredMethod.methodType() == QUERY_TYPE + ? rootQueryType + : rootMutationType; + + // add all the arguments and check to see if they contain types that are not yet known + // this check is done no matter if the field definition is going to be created or not + for (SchemaArgument a : discoveredMethod.arguments()) { + String originalTypeName = a.argumentType(); + String typeName = getGraphQLType(originalTypeName); + + a.argumentType(typeName); + String returnType = a.argumentType(); + + if (originalTypeName.equals(returnType) && !ID.equals(returnType)) { + // type name has not changed, so this must be either a Scalar, Enum or a Type + // Note: Interfaces are not currently supported as InputTypes in 1.0 of the Specification + // if is Scalar or enum then add to unresolved types and they will be dealt with + if (getScalar(returnType) != null || isEnumClass(returnType)) { + setUnresolvedTypes.add(returnType); + } else { + // create the input Type here + SchemaInputType inputType = generateType(returnType, true).createInputType(""); + // if the name of the InputType was not changed then append "Input" + if (inputType.name().equals(Class.forName(returnType).getSimpleName())) { + inputType.name(inputType.name() + "Input"); + } + + if (!schema.containsInputTypeWithName(inputType.name())) { + schema.addInputType(inputType); + checkInputType(schema, inputType); + } + a.argumentType(inputType.name()); + } + } + if (fd != null) { + fd.addArgument(a); + } + } + + if (fd != null) { + DataFetcher dataFetcher = null; + String[] format = discoveredMethod.format(); + SchemaScalar dateScalar = getScalar(discoveredMethod.returnType()); + + // if the type is a Date/Time/DateTime scalar and there is currently no format, + // then use the default format if there is one + if ((dateScalar != null && isDateTimeScalar(dateScalar.name()) && isFormatEmpty(format))) { + Class originalType = fd.isArrayReturnType() ? fd.originalArrayType() : fd.originalType(); + String[] newFormat = ensureFormat(dateScalar.name(), + originalType.getName(), new String[2]); + if (newFormat.length == 2) { + format = new String[] {DATE, newFormat[0], newFormat[1]}; + } + } + if (!isFormatEmpty(format)) { + // a format exists on the method return type so format it after returning the value + final String graphQLType = getGraphQLType(fd.returnType()); + final DataFetcher methodDataFetcher = DataFetcherUtils.newMethodDataFetcher(schema, clazz, method, null, + fd.arguments().toArray( + new SchemaArgument[0])); + final String[] newFormat = new String[] {format[0], format[1], format[2]}; + + if (dateScalar != null && isDateTimeScalar(dateScalar.name())) { + dataFetcher = DataFetcherFactories.wrapDataFetcher(methodDataFetcher, + (e, v) -> { + DateTimeFormatter dateTimeFormatter = + getCorrectDateFormatter( + graphQLType, newFormat[2], + newFormat[1]); + return formatDate(v, dateTimeFormatter); + }); + } else { + dataFetcher = DataFetcherFactories.wrapDataFetcher(methodDataFetcher, + (e, v) -> { + NumberFormat numberFormat = getCorrectNumberFormat( + graphQLType, newFormat[2], newFormat[1]); + boolean isScalar = SchemaGeneratorHelper.getScalar( + discoveredMethod.returnType()) != null; + return formatNumber(v, isScalar, numberFormat); + }); + fd.returnType(STRING); + } + } else { + // no formatting, just call the method + dataFetcher = DataFetcherUtils.newMethodDataFetcher(schema, clazz, method, null, + fd.arguments().toArray(new SchemaArgument[0])); + } + fd.dataFetcher(dataFetcher); + fd.description(discoveredMethod.description()); + + schemaType.addFieldDefinition(fd); + + checkScalars(schema, schemaType); + + String returnType = discoveredMethod.returnType(); + // check to see if this is a known type + if (returnType.equals(fd.returnType()) && !setUnresolvedTypes.contains(returnType) + && !ID.equals(returnType)) { + // value class was unchanged meaning we need to resolve + setUnresolvedTypes.add(returnType); + } + } + } + } + + /** + * Return true if the format is empty or undefined. + * + * @param format format to check + * @return true if the format is empty or undefined + */ + protected static boolean isFormatEmpty(String[] format) { + if (format == null || format.length == 0) { + return true; + } + // now check that each value is not null + for (String entry : format) { + if (entry == null) { + return true; + } + } + return false; + } + + /** + * Check this new {@link SchemaInputType} contains any types, then they must also have InputTypes created for them if they are + * not enums or scalars. + * + * @param schema {@link Schema} to add to + * @param schemaInputType {@link SchemaInputType} to check + * @throws IntrospectionException if issues with introspection + * @throws ClassNotFoundException if class not found + */ + private void checkInputType(Schema schema, SchemaInputType schemaInputType) + throws IntrospectionException, ClassNotFoundException { + // if this new Type contains any types, then they must also have + // InputTypes created for them if they are not enums or scalars + Set setInputTypes = new HashSet<>(); + setInputTypes.add(schemaInputType); + + while (setInputTypes.size() > 0) { + SchemaInputType type = setInputTypes.iterator().next(); + setInputTypes.remove(type); + + // check each field definition to see if any return types are unknownInputTypes + for (SchemaFieldDefinition fdi : type.fieldDefinitions()) { + String fdReturnType = fdi.returnType(); + + if (!isGraphQLType(fdReturnType)) { + // must be either an unknown input type, Scalar or Enum + if (getScalar(fdReturnType) != null || isEnumClass(fdReturnType)) { + setUnresolvedTypes.add(fdReturnType); + } else { + // must be a type, create a new input Type but do not add it to + // the schema if it already exists + SchemaInputType newInputType = generateType(fdReturnType, true).createInputType(""); + + // if the name of the InputType was not changed then append "Input" + if (newInputType.name().equals(Class.forName(newInputType.valueClassName()).getSimpleName())) { + newInputType.name(newInputType.name() + "Input"); + } + + if (!schema.containsInputTypeWithName(newInputType.name())) { + schema.addInputType(newInputType); + setInputTypes.add(newInputType); + } + fdi.returnType(newInputType.name()); + } + } + } + } + } + + /** + * Add the given {@link SchemaType} to the {@link Schema}. + * + * @param schema the {@link Schema} to add to + * @throws IntrospectionException if issues with introspection + * @throws ClassNotFoundException if class not found + */ + private void addTypeToSchema(Schema schema, SchemaType type) + throws IntrospectionException, ClassNotFoundException { + + String valueClassName = type.valueClassName(); + retrieveGetterBeanMethods(Class.forName(valueClassName), false).forEach((k, v) -> { + SchemaFieldDefinition fd = newFieldDefinition(v, null); + type.addFieldDefinition(fd); + + checkScalars(schema, type); + + String returnType = v.returnType(); + // check to see if this is a known type + if (!ID.equals(returnType) && returnType.equals(fd.returnType()) && !setUnresolvedTypes.contains(returnType)) { + // value class was unchanged meaning we need to resolve + setUnresolvedTypes.add(returnType); + } + }); + + // check if this Type is an interface then obtain all concrete classes that implement the type + // and add them to the set of unresolved types + if (type.isInterface()) { + Collection> setConcreteClasses = jandexUtils.getKnownImplementors(valueClassName); + setConcreteClasses.forEach(c -> setUnresolvedTypes.add(c.getName())); + } + + schema.addType(type); + } + + /** + * Generate an {@link SchemaEnum} from a given {@link java.lang.Enum}. + * + * @param clazz the {@link java.lang.Enum} to introspect + * @return a new {@link SchemaEnum} or null if the class provided is not an {@link java.lang.Enum} + */ + private SchemaEnum generateEnum(Class clazz) { + if (clazz.isEnum()) { + SchemaEnum newSchemaEnum = SchemaEnum.builder().name(getTypeName(clazz)).build(); + + Arrays.stream(clazz.getEnumConstants()) + .map(Object::toString) + .forEach(newSchemaEnum::addValue); + return newSchemaEnum; + } + return null; + } + + /** + * Return a new {@link SchemaFieldDefinition} with the given field and class. + * + * @param discoveredMethod the {@link DiscoveredMethod} + * @param optionalName optional name for the field definition + * @return a {@link SchemaFieldDefinition} + */ + @SuppressWarnings("rawtypes") + private SchemaFieldDefinition newFieldDefinition(DiscoveredMethod discoveredMethod, + String optionalName) { + String valueClassName = discoveredMethod.returnType(); + String graphQLType = getGraphQLType(valueClassName); + DataFetcher dataFetcher = null; + String propertyName = discoveredMethod.propertyName(); + String name = discoveredMethod.name(); + + boolean isArrayReturnType = discoveredMethod.isArrayReturnType() || discoveredMethod.isCollectionType() + || discoveredMethod.isMap(); + + if (isArrayReturnType) { + if (discoveredMethod.isMap()) { + // add DataFetcher that will just retrieve the values() from the Map. + // The microprofile-graphql spec does not specify how Maps are treated + // and leaves this up to the individual implementation. This implementation + // supports returning the values of the map only and does not support using + // Maps or types with Maps as return values from a query or mutation + dataFetcher = DataFetcherUtils.newMapValuesDataFetcher(propertyName); + } + } + + // check for format on the property + // note: currently the format will be an array of [3] as defined by FormattingHelper.getFormattingAnnotation + String[] format = discoveredMethod.format(); + if (propertyName != null && !isFormatEmpty(format)) { + if (!isGraphQLType(valueClassName)) { + dataFetcher = retrieveFormattingDataFetcher(format, propertyName, graphQLType); + + // if the format is a number format then set the return type to String + if (NUMBER.equals(format[0])) { + graphQLType = STRING; + } + } + } else { + // Add a PropertyDataFetcher if the name has been changed via annotation + if (propertyName != null && !propertyName.equals(name)) { + dataFetcher = new PropertyDataFetcher(propertyName); + } + } + + SchemaFieldDefinition fd = SchemaFieldDefinition.builder() + .name(optionalName != null + ? optionalName + : discoveredMethod.name()) + .returnType(graphQLType) + .arrayReturnType(isArrayReturnType) + .returnTypeMandatory(discoveredMethod.isReturnTypeMandatory()) + .arrayLevels(discoveredMethod.arrayLevels()) + .dataFetcher(dataFetcher) + .originalType(discoveredMethod.method().getReturnType()) + .arrayReturnTypeMandatory(discoveredMethod.isArrayReturnTypeMandatory()) + .originalArrayType(isArrayReturnType ? discoveredMethod.originalArrayType() : null) + .build(); + + if (format != null && format.length == 3) { + fd.format(new String[] {format[1], format[2]}); + } + + fd.description(discoveredMethod.description()); + fd.jsonbFormat(discoveredMethod.isJsonbFormat()); + fd.defaultValue(discoveredMethod.defaultValue()); + fd.jsonbProperty(discoveredMethod.isJsonbProperty()); + return fd; + } + + /** + * Return the correct formatting {@link DataFetcher} to format the date or number field. + * + * @param rawFormat raw format with [0] = type, [1] = locale, [2] = format + * @param propertyName property to fetch from + * @param type the type of the data - from Class.getName() + * @return the correct {@link DataFetcher} + */ + private DataFetcher retrieveFormattingDataFetcher(String[] rawFormat, String propertyName, String type) { + return NUMBER.equals(rawFormat[0]) + ? new DataFetcherUtils.NumberFormattingDataFetcher(propertyName, type, rawFormat[1], rawFormat[2]) + : new DataFetcherUtils.DateFormattingDataFetcher(propertyName, type, rawFormat[1], rawFormat[2]); + } + + /** + * Look in the given {@link Schema} for any field definitions, arguments and key value classes that contain the return type of + * the long return type and replace with short return type. + * + * @param schema schema to introspect + * @param longReturnType long return type + * @param shortReturnType short return type + */ + @SuppressWarnings("unchecked") + private void updateLongTypes(Schema schema, String longReturnType, String shortReturnType) { + // concatenate both the SchemaType and SchemaInputType + Stream streamInputTypes = schema.getInputTypes().stream().map(it -> (SchemaType) it); + Stream streamAll = Stream.concat(streamInputTypes, schema.getTypes().stream()); + streamAll.forEach(t -> { + t.fieldDefinitions().forEach(fd -> { + if (fd.returnType().equals(longReturnType)) { + fd.returnType(shortReturnType); + } + + // check arguments + fd.arguments().forEach(a -> { + if (a.argumentType().equals(longReturnType)) { + a.argumentType(shortReturnType); + } + }); + }); + }); + + // look through set of additional methods added for Source annotations + setAdditionalMethods.forEach(m -> { + m.arguments().forEach(a -> { + if (a.argumentType().equals(longReturnType)) { + a.argumentType(shortReturnType); + } + }); + }); + } + + /** + * Return a {@link Map} of all the discovered methods which have the {@link Query} or {@link Mutation} annotations. + * + * @param clazz Class to introspect + * @return a {@link Map} of the methods and return types + * + * @throws IntrospectionException if any errors with introspection + * @throws ClassNotFoundException if any classes are not found + */ + protected Map retrieveAllAnnotatedBeanMethods(Class clazz) + throws IntrospectionException, ClassNotFoundException { + Map mapDiscoveredMethods = new HashMap<>(); + for (Method m : getAllMethods(clazz)) { + boolean isQuery = m.getAnnotation(Query.class) != null; + boolean isMutation = m.getAnnotation(Mutation.class) != null; + boolean hasSourceAnnotation = Arrays.stream(m.getParameters()).anyMatch(p -> p.getAnnotation(Source.class) != null); + if (isMutation && isQuery) { + ensureConfigurationException(LOGGER, "The class " + clazz.getName() + + " may not have both a Query and Mutation annotation"); + } + if (isQuery || isMutation || hasSourceAnnotation) { + DiscoveredMethod discoveredMethod = generateDiscoveredMethod(m, clazz, null, false, true); + discoveredMethod.methodType(isQuery || hasSourceAnnotation ? QUERY_TYPE : MUTATION_TYPE); + String name = discoveredMethod.name(); + if (mapDiscoveredMethods.containsKey(name)) { + ensureConfigurationException(LOGGER, "A method named " + name + " already exists on " + + "the " + (isMutation ? "mutation" : "query") + + " " + discoveredMethod.method().getName()); + } + mapDiscoveredMethods.put(name, discoveredMethod); + } + } + return mapDiscoveredMethods; + } + + /** + * Retrieve only the getter methods for the {@link Class}. + * + * @param clazz the {@link Class} to introspect + * @param isInputType indicates if this type is an input type + * @return a {@link Map} of the methods and return types + * @throws IntrospectionException if there were errors introspecting classes + * @throws ClassNotFoundException if any class is not found + */ + protected Map retrieveGetterBeanMethods(Class clazz, + boolean isInputType) + throws IntrospectionException, ClassNotFoundException { + Map mapDiscoveredMethods = new HashMap<>(); + + for (Method m : getAllMethods(clazz)) { + if (m.getName().equals("getClass") || shouldIgnoreMethod(m, isInputType)) { + continue; + } + + Optional optionalPdReadMethod = Arrays + .stream(Introspector.getBeanInfo(clazz).getPropertyDescriptors()) + .filter(p -> p.getReadMethod() != null && p.getReadMethod().getName().equals(m.getName())).findFirst(); + + if (optionalPdReadMethod.isPresent()) { + PropertyDescriptor propertyDescriptor = optionalPdReadMethod.get(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + boolean ignoreWriteMethod = isInputType && writeMethod != null && shouldIgnoreMethod(writeMethod, true); + + // only include if the field should not be ignored + if (!shouldIgnoreField(clazz, propertyDescriptor.getName()) && !ignoreWriteMethod) { + // this is a getter method, include it here + DiscoveredMethod discoveredMethod = + generateDiscoveredMethod(m, clazz, propertyDescriptor, isInputType, false); + mapDiscoveredMethods.put(discoveredMethod.name(), discoveredMethod); + } + } + } + return mapDiscoveredMethods; + } + + /** + * Return all {@link Method}s for a given {@link Class}. + * + * @param clazz the {@link Class} to introspect + * @return all {@link Method}s for a given {@link Class} + * + * @throws IntrospectionException if any errors with introspection + */ + protected List getAllMethods(Class clazz) throws IntrospectionException { + return Arrays.asList(Introspector.getBeanInfo(clazz).getMethodDescriptors()) + .stream() + .map(MethodDescriptor::getMethod) + .collect(Collectors.toList()); + } + + /** + * Generate a {@link DiscoveredMethod} from the given arguments. + * + * @param method {@link Method} being introspected + * @param clazz {@link Class} being introspected + * @param pd {@link PropertyDescriptor} for the property being introspected (may be null if retrieving all + * methods as in the case for a {@link Query} annotation) + * @param isInputType indicates if the method is part of an input type + * @param isQueryOrMutation indicates if this is for a query or mutation + * @return a {@link DiscoveredMethod} + * @throws ClassNotFoundException if any class is not found + */ + private DiscoveredMethod generateDiscoveredMethod(Method method, Class clazz, + PropertyDescriptor pd, boolean isInputType, + boolean isQueryOrMutation) throws ClassNotFoundException { + String[] format = new String[0]; + String description = null; + boolean isReturnTypeMandatory = false; + boolean isArrayReturnTypeMandatory = false; + boolean isJsonbFormat = false; + boolean isJsonbProperty; + String defaultValue = null; + String varName = stripMethodName(method, !isQueryOrMutation); + + String annotatedName = getMethodName(isInputType ? pd.getWriteMethod() : method); + if (annotatedName != null) { + varName = annotatedName; + } else if (pd != null) { // check the field only if this is a getter + annotatedName = getFieldName(clazz, pd.getName()); + if (annotatedName != null) { + varName = annotatedName; + } + } + + Method methodToCheck = isInputType ? pd.getWriteMethod() : method; + isJsonbProperty = methodToCheck != null && methodToCheck.getAnnotation(JsonbProperty.class) != null; + + ensureValidName(LOGGER, varName); + + Class returnClazz = method.getReturnType(); + String returnClazzName = returnClazz.getName(); + ensureNonVoidQueryOrMutation(returnClazzName, method, clazz); + + if (pd != null) { + boolean fieldHasIdAnnotation = false; + Field field = null; + + try { + field = clazz.getDeclaredField(pd.getName()); + fieldHasIdAnnotation = field != null && field.getAnnotation(Id.class) != null; + description = getDescription(field.getAnnotation(Description.class)); + defaultValue = isInputType ? getDefaultValueAnnotationValue(field) : null; // only make sense for input types + NonNull nonNullAnnotation = field.getAnnotation(NonNull.class); + isArrayReturnTypeMandatory = getAnnotationValue(getFieldAnnotations(field, 0), NonNull.class) != null; + + if (isInputType) { + Method writeMethod = pd.getWriteMethod(); + if (writeMethod != null) { // retrieve the setter method and check the description + String methodDescription = getDescription(writeMethod.getAnnotation(Description.class)); + if (methodDescription != null) { + description = methodDescription; + } + String writeMethodDefaultValue = getDefaultValueAnnotationValue(writeMethod); + if (writeMethodDefaultValue != null) { + defaultValue = writeMethodDefaultValue; + } + + NonNull methodAnnotation = writeMethod.getAnnotation(NonNull.class); // for an input type the + if (methodAnnotation != null) { // method annotation will override + nonNullAnnotation = methodAnnotation; + } + + // the annotation on the set method parameter will override for the input type if it's present + boolean isSetArrayMandatory = + getAnnotationValue(getParameterAnnotations(writeMethod.getParameters()[0], 0), + NonNull.class) != null; + if (isSetArrayMandatory && !isArrayReturnTypeMandatory) { + isArrayReturnTypeMandatory = true; + } + + // if the set method has a format then this should overwrite any formatting as this is an InputType + String[] writeMethodFormat = getFormattingAnnotation(writeMethod); + if (!isFormatEmpty(writeMethodFormat)) { + format = writeMethodFormat; + isJsonbFormat = isJsonbAnnotationPresent(writeMethod); + isJsonbProperty = writeMethod.getAnnotation(JsonbProperty.class) != null; + } + + Parameter[] parameters = writeMethod.getParameters(); + if (parameters.length == 1) { + String[] argumentTypeFormat = FormattingHelper.getMethodParameterFormat(parameters[0], 0); + if (!isFormatEmpty(argumentTypeFormat)) { + format = argumentTypeFormat; + isJsonbFormat = isJsonbAnnotationPresent(parameters[0]); + isJsonbProperty = parameters[0].getAnnotation(JsonbProperty.class) != null; + } + } + } + } else { + NonNull methodAnnotation = method.getAnnotation(NonNull.class); + if (methodAnnotation != null) { + nonNullAnnotation = methodAnnotation; + } + if (!isArrayReturnTypeMandatory) { + isArrayReturnTypeMandatory = + getAnnotationValue(getMethodAnnotations(method, 0), NonNull.class) != null; + } + } + + isReturnTypeMandatory = (isPrimitive(returnClazzName) && defaultValue == null) + || nonNullAnnotation != null && defaultValue == null; + + } catch (NoSuchFieldException ignored) { + LOGGER.fine("No such field " + pd.getName() + " on class " + clazz.getName()); + } + + if (fieldHasIdAnnotation || method.getAnnotation(Id.class) != null) { + validateIDClass(returnClazz); + returnClazzName = ID; + } + + if (field != null && isFormatEmpty(format)) { // check for format on the property + format = getFormattingAnnotation(field); // but only override if it is null + if (isFormatEmpty(format)) { // check format of the inner most class. E.g. List<@DateFormat("DD/MM") String> + format = FormattingHelper.getFieldFormat(field, 0); + } + isJsonbFormat = isJsonbAnnotationPresent(field); + } + } else { // pd is null which means this is for query or mutation + defaultValue = getDefaultValueAnnotationValue(method); + isReturnTypeMandatory = isPrimitive(returnClazzName) && defaultValue == null + || method.getAnnotation(NonNull.class) != null && defaultValue == null; + if (method.getAnnotation(Id.class) != null) { + validateIDClass(returnClazz); + returnClazzName = ID; + } + } + + // check for method return type number format + String[] methodFormat = getFormattingAnnotation(method); + if (methodFormat[0] != null && !isInputType) { + format = methodFormat; + } + + DiscoveredMethod discoveredMethod = DiscoveredMethod.builder().name(varName).method(method).format(format) + .defaultValue(defaultValue).jsonbFormat(isJsonbFormat).jsonbProperty(isJsonbProperty) + .propertyName(pd != null ? pd.getName() : null).build(); + + if (description == null && !isInputType) { + description = getDescription(method.getAnnotation(Description.class)); + } + + processMethodParameters(method, discoveredMethod, annotatedName); + ReturnType realReturnType = getReturnType(returnClazz, method.getGenericReturnType(), -1, method); + processReturnType(discoveredMethod, realReturnType, returnClazzName, isInputType, varName, method); + + discoveredMethod.returnTypeMandatory(isReturnTypeMandatory); + discoveredMethod.arrayReturnTypeMandatory(isArrayReturnTypeMandatory + || realReturnType.isReturnTypeMandatory && !isInputType); + discoveredMethod.description(description); + + return discoveredMethod; + } + + /** + * Ensure that the query or mutation does not return void. + * @param returnClazzName return class name + * @param method {@link Method} being processed + * @param clazz {@link Class} being processed + */ + private void ensureNonVoidQueryOrMutation(String returnClazzName, Method method, Class clazz) { + if ("void".equals(returnClazzName)) { + ensureConfigurationException(LOGGER, "void is not a valid return type for a Query or Mutation method '" + + method.getName() + "' on class " + clazz.getName()); + } + } + + /** + * Process the {@link ReturnType} and update {@link DiscoveredMethod} as required. + * @param discoveredMethod {@link DiscoveredMethod} + * @param realReturnType {@link ReturnType} with details of the return types + * @param returnClazzName return class name + * @param isInputType indicates if the method is part of an input type + * @param varName name of the variable + * @param method {@link Method} being processed + * + * @throws ClassNotFoundException if any class is not found + */ + private void processReturnType(DiscoveredMethod discoveredMethod, ReturnType realReturnType, + String returnClazzName, boolean isInputType, + String varName, Method method) throws ClassNotFoundException { + if (realReturnType.returnClass() != null && !ID.equals(returnClazzName)) { + discoveredMethod.arrayReturnType(realReturnType.isArrayType()); + discoveredMethod.collectionType(realReturnType.collectionType()); + discoveredMethod.map(realReturnType.isMap()); + SchemaScalar dateScalar = getScalar(realReturnType.returnClass()); + if (dateScalar != null && isDateTimeScalar(dateScalar.name())) { + // only set the original array type if it's a date/time + discoveredMethod.originalArrayType(Class.forName(realReturnType.returnClass)); + } else if (discoveredMethod.isArrayReturnType()) { + Class originalArrayType = getSafeClass(realReturnType.returnClass); + if (originalArrayType != null) { + discoveredMethod.originalArrayType(originalArrayType); + } + } + discoveredMethod.returnType(realReturnType.returnClass()); + // only override if this is not an input type + if (!isInputType && !isFormatEmpty(realReturnType.format())) { + discoveredMethod.format(realReturnType.format); + } + } else { + discoveredMethod.name(varName); + discoveredMethod.returnType(returnClazzName); + discoveredMethod.method(method); + } + + discoveredMethod.arrayLevels(realReturnType.arrayLevels()); + } + + /** + * Process parameters for the given method. + * + * @param method {@link Method} to process + * @param discoveredMethod {@link DiscoveredMethod} to update + * @param annotatedName annotated name or null + */ + private void processMethodParameters(Method method, DiscoveredMethod discoveredMethod, String annotatedName) { + Parameter[] parameters = method.getParameters(); + if (parameters != null && parameters.length > 0) { + java.lang.reflect.Type[] genericParameterTypes = method.getGenericParameterTypes(); + int i = 0; + for (Parameter parameter : parameters) { + boolean isID = false; + Name paramNameAnnotation = parameter.getAnnotation(Name.class); + String parameterName = paramNameAnnotation != null + && !paramNameAnnotation.value().isBlank() + ? paramNameAnnotation.value() + : parameter.getName(); + + Class paramType = parameter.getType(); + + ReturnType returnType = getReturnType(paramType, genericParameterTypes[i], i, method); + + if (parameter.getAnnotation(Id.class) != null) { + validateIDClass(returnType.returnClass()); + returnType.returnClass(ID); + isID = true; + } + + String argumentDefaultValue = getDefaultValueAnnotationValue(parameter); + + boolean isMandatory = + (isPrimitive(paramType) && argumentDefaultValue == null) + || (parameter.getAnnotation(NonNull.class) != null && argumentDefaultValue == null); + SchemaArgument argument = SchemaArgument.builder() + .argumentName(parameterName) + .argumentType(returnType.returnClass()) + .mandatory(isMandatory) + .defaultValue(argumentDefaultValue) + .originalType(paramType) + .description(getDescription(parameter.getAnnotation(Description.class))) + .build(); + + String[] argumentFormat = getFormattingAnnotation(parameter); + String[] argumentTypeFormat = FormattingHelper.getMethodParameterFormat(parameter, 0); + + // The argument type format overrides any argument format. E.g. NumberFormat should apply below + // E.g. public List getListAsString(@Name("arg1") + // @JsonbNumberFormat("ignore 00.0000000") + // List> values) + argumentFormat = !isFormatEmpty(argumentTypeFormat) ? argumentTypeFormat : argumentFormat; + + if (argumentFormat[0] != null) { + argument.format(new String[] {argumentFormat[1], argumentFormat[2]}); + argument.argumentType(String.class.getName()); + } + + Source sourceAnnotation = parameter.getAnnotation(Source.class); + if (sourceAnnotation != null) { + // set the method name to the correct property name as it will currently be incorrect + discoveredMethod.name(annotatedName != null ? annotatedName : stripMethodName(method, false)); + discoveredMethod.source(returnType.returnClass()); + discoveredMethod.queryAnnotated(method.getAnnotation(Query.class) != null); + argument.sourceArgument(true); + } + + if (!isID) { + SchemaScalar dateScalar = getScalar(returnType.returnClass()); + if (dateScalar != null && isDateTimeScalar(dateScalar.name())) { + // only set the original array type if it's a date/time + discoveredMethod.originalArrayType(getSafeClass(returnType.returnClass)); + } + argument.arrayReturnTypeMandatory(returnType.isReturnTypeMandatory); + argument.arrayReturnType(returnType.isArrayType); + if (returnType.isArrayType) { + argument.originalArrayType(getSafeClass(returnType.returnClass)); + } + argument.arrayLevels(returnType.arrayLevels()); + } + + discoveredMethod.addArgument(argument); + i++; + } + } + } + + /** + * Return the {@link ReturnType} for this return class and method. + * + * @param returnClazz return type + * @param genericReturnType generic return {@link java.lang.reflect.Type} may be null + * @param parameterNumber the parameter number for the parameter + * @param method {@link Method} to find parameter for + * @return a {@link ReturnType} + */ + protected ReturnType getReturnType(Class returnClazz, java.lang.reflect.Type genericReturnType, + int parameterNumber, Method method) { + ReturnType actualReturnType = ReturnType.create(); + RootTypeResult rootTypeResult; + String returnClazzName = returnClazz.getName(); + boolean isCollection = Collection.class.isAssignableFrom(returnClazz); + boolean isMap = Map.class.isAssignableFrom(returnClazz); + // deal with Collection or Map + if (isCollection || isMap) { + if (isCollection) { + actualReturnType.collectionType(returnClazzName); + } + + actualReturnType.map(isMap); + // index is 0 for Collection and 1 for Map which assumes we are not + // interested in the map K, just the map V which is what our implementation will do + rootTypeResult = getRootTypeName(genericReturnType, isCollection ? 0 : 1, parameterNumber, method); + String rootType = rootTypeResult.rootTypeName(); + + // set the initial number of array levels to the levels of the root type + int arrayLevels = rootTypeResult.levels(); + + if (isArrayType(rootType)) { + actualReturnType.returnClass(getRootArrayClass(rootType)); + arrayLevels += getArrayLevels(rootType); + } else { + actualReturnType.returnClass(rootType); + } + actualReturnType.arrayLevels(arrayLevels); + actualReturnType.returnTypeMandatory(rootTypeResult.isArrayReturnTypeMandatory()); + actualReturnType.format(rootTypeResult.format); + actualReturnType.arrayType(true); + } else if (!returnClazzName.isEmpty() && returnClazzName.startsWith("[")) { + // return type is array of either primitives or Objects/Interface/Enum. + actualReturnType.arrayType(true); + actualReturnType.arrayLevels(getArrayLevels(returnClazzName)); + actualReturnType.returnClass(getRootArrayClass(returnClazzName)); + } else { + // primitive or type + actualReturnType.returnClass(returnClazzName); + } + return actualReturnType; + } + + /** + * Return the inner most root type such as {@link String} for a List of List of String. + * + * @param genericReturnType the {@link java.lang.reflect.Type} + * @param index the index to use, either 0 for {@link Collection} or 1 for {@link Map} + * @param parameterNumber parameter number or -1 if parameter not being checked + * @param method {@link Method} being checked + * @return the inner most root type + */ + protected RootTypeResult getRootTypeName(java.lang.reflect.Type genericReturnType, int index, + int parameterNumber, Method method) { + int level = 1; + boolean isParameter = parameterNumber != -1; + String[] format = NO_FORMATTING; + RootTypeResult.Builder builder = RootTypeResult.builder(); + + boolean isReturnTypeMandatory; + if (genericReturnType instanceof ParameterizedType) { + ParameterizedType paramReturnType = (ParameterizedType) genericReturnType; + // loop until we get the actual return type in the case we have List> + java.lang.reflect.Type actualTypeArgument = paramReturnType.getActualTypeArguments()[index]; + while (actualTypeArgument instanceof ParameterizedType) { + level++; + ParameterizedType parameterizedType2 = (ParameterizedType) actualTypeArgument; + actualTypeArgument = parameterizedType2.getActualTypeArguments()[index]; + } + + Class clazz = actualTypeArgument.getClass(); + boolean hasAnnotation = false; + if (isParameter) { + // check for the NonNull + Parameter parameter = method.getParameters()[parameterNumber]; + hasAnnotation = getAnnotationValue(getParameterAnnotations(parameter, 0), NonNull.class) != null; + } else { + format = FormattingHelper.getMethodFormat(method, 0); + } + + isReturnTypeMandatory = hasAnnotation || isPrimitive(clazz.getName()); + return builder.rootTypeName(((Class) actualTypeArgument).getName()) + .levels(level) + .arrayReturnTypeMandatory(isReturnTypeMandatory) + .format(format) + .build(); + } else { + Class clazz = genericReturnType.getClass(); + isReturnTypeMandatory = clazz.getAnnotation(NonNull.class) != null + || isPrimitive(clazz.getName()); + return builder.rootTypeName(((Class) genericReturnType).getName()) + .levels(level) + .arrayReturnTypeMandatory(isReturnTypeMandatory) + .format(format) + .build(); + } + } + + /** + * Return the {@link JandexUtils} instance. + * + * @return the {@link JandexUtils} instance. + */ + protected JandexUtils getJandexUtils() { + return jandexUtils; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaGenerator}. + */ + public static class Builder implements io.helidon.common.Builder { + + private final Set> collectedApis = new HashSet<>(); + + /** + * Build the instance from this builder. + * + * @return instance of the built type + */ + @Override + public SchemaGenerator build() { + return new SchemaGenerator(this); + } + + public Builder classes(Set> collectedApis) { + this.collectedApis.addAll(collectedApis); + return this; + } + } + + /** + * Defines return types for methods or parameters. + */ + public static class ReturnType { + + /** + * Return class. + */ + private String returnClass; + + /** + * Indicates if this is an array type. + */ + private boolean isArrayType = false; + + /** + * Indicates if this is a {@link Map}. + */ + private boolean isMap = false; + + /** + * Return the type of collection. + */ + private String collectionType; + + /** + * Number of levels in the Array. + */ + private int arrayLevels = 0; + + /** + * Indicates id the return type is mandatory. + */ + private boolean isReturnTypeMandatory; + + /** + * The format of the return type if any. + */ + private String[] format; + + /** + * Default constructor. + */ + private ReturnType() { + } + + /** + * Create a new {@link ReturnType}. + * @return a new {@link ReturnType} + */ + public static ReturnType create() { + return new ReturnType(); + } + + /** + * Return the return class. + * + * @return the return class + */ + public String returnClass() { + return returnClass; + } + + /** + * Sets the return class. + * + * @param returnClass the return class + */ + public void returnClass(String returnClass) { + this.returnClass = returnClass; + } + + /** + * Indicates if this is an array type. + * + * @return if this is an array type + */ + public boolean isArrayType() { + return isArrayType; + } + + /** + * Set if this is an array type. + * + * @param arrayType if this is an array type + */ + public void arrayType(boolean arrayType) { + isArrayType = arrayType; + } + + /** + * Indicates if this is a {@link Map}. + * + * @return if this is a {@link Map} + */ + public boolean isMap() { + return isMap; + } + + /** + * Set if this is a {@link Map}. + * + * @param map if this is a {@link Map} + */ + public void map(boolean map) { + isMap = map; + } + + /** + * Return the type of collection. + * + * @return the type of collection + */ + public String collectionType() { + return collectionType; + } + + /** + * Set the type of collection. + * + * @param collectionType the type of collection + */ + public void collectionType(String collectionType) { + this.collectionType = collectionType; + } + + /** + * Return the level of arrays or 0 if not an array. + * + * @return the level of arrays + */ + public int arrayLevels() { + return arrayLevels; + } + + /** + * Set the level of arrays or 0 if not an array. + * + * @param arrayLevels the level of arrays or 0 if not an array + */ + public void arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + } + + /** + * Indicates if the return type is mandatory. + * + * @return if the return type is mandatory + */ + public boolean isReturnTypeMandatory() { + return isReturnTypeMandatory; + } + + /** + * Set if the return type is mandatory. + * + * @param returnTypeMandatory if the return type is mandatory + */ + public void returnTypeMandatory(boolean returnTypeMandatory) { + isReturnTypeMandatory = returnTypeMandatory; + } + + /** + * Return the format of the result class. + * + * @return the format of the result class + */ + public String[] format() { + if (format == null) { + return null; + } + String[] copy = new String[format.length]; + System.arraycopy(format, 0, copy, 0, copy.length); + return copy; + } + + /** + * Set the format of the result class. + * + * @param format the format of the result class + */ + public void format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + } + } + + /** + * Represents a result for the method getRootTypeName. + */ + public static class RootTypeResult { + + /** + * The root type of the {@link Collection} or {@link Map}. + */ + private final String rootTypeName; + + /** + * The number of levels in total. + */ + private final int levels; + + /** + * Indicates if the array return type is mandatory. + */ + private boolean isArrayReturnTypeMandatory; + + /** + * The format of the result class. + */ + private final String[] format; + + /** + * Construct a {@link RootTypeResult}. + * + * @param builder the {@link Builder} to construct from + */ + private RootTypeResult(Builder builder) { + this.rootTypeName = builder.rootTypeName; + this.levels = builder.levels; + this.isArrayReturnTypeMandatory = builder.isArrayReturnTypeMandatory; + this.format = builder.format; + } + + /** + * Fluent API builder to create {@link RootTypeResult}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the root type of the {@link Collection} or {@link Map}. + * + * @return root type of the {@link Collection} or {@link Map} + */ + public String rootTypeName() { + return rootTypeName; + } + + /** + * Return the number of levels in total. + * + * @return the number of levels in total + */ + public int levels() { + return levels; + } + + /** + * Indicates if the return type is mandatory. + * + * @return if the return type is mandatory + */ + public boolean isArrayReturnTypeMandatory() { + return isArrayReturnTypeMandatory; + } + + /** + * Return the format of the result class. + * + * @return the format of the result class + */ + public String[] format() { + if (format == null) { + return null; + } + String[] copy = new String[format.length]; + System.arraycopy(format, 0, copy, 0, copy.length); + return copy; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link DiscoveredMethod}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String rootTypeName; + private int levels; + private boolean isArrayReturnTypeMandatory; + private String[] format; + + /** + * Set the root type of the {@link Collection} or {@link Map}. + * + * @param rootTypeName root type of the {@link Collection} or {@link Map} + * @return updated builder instance + */ + public Builder rootTypeName(String rootTypeName) { + this.rootTypeName = rootTypeName; + return this; + } + + /** + * Set the number of array levels if return type is an array. + * + * @param levels the number of array levels if return type is an array + * @return updated builder instance + */ + public Builder levels(int levels) { + this.levels = levels; + return this; + } + + /** + * Set if the value of the array is mandatory. + * + * @param isArrayReturnTypeMandatory If the return type is an array then indicates if the value in the array is + * mandatory + * @return updated builder instance + */ + public Builder arrayReturnTypeMandatory(boolean isArrayReturnTypeMandatory) { + this.isArrayReturnTypeMandatory = isArrayReturnTypeMandatory; + return this; + } + + /** + * Set the format for a number or date. + * + * @param format the format for a number or date + * @return updated builder instance + */ + public Builder format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + + return this; + } + + /** + * Build the instance from this builder. + * + * @return instance of the built type + */ + @Override + public RootTypeResult build() { + return new RootTypeResult(this); + } + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGeneratorHelper.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGeneratorHelper.java new file mode 100644 index 00000000000..35a3acff0c0 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaGeneratorHelper.java @@ -0,0 +1,1966 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; + +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTransient; + +import graphql.scalars.ExtendedScalars; +import graphql.scalars.object.ObjectScalar; +import org.eclipse.microprofile.graphql.DefaultValue; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.Enum; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Ignore; +import org.eclipse.microprofile.graphql.Input; +import org.eclipse.microprofile.graphql.Interface; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; +import org.eclipse.microprofile.graphql.Type; + +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_BIGDECIMAL_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_BIGINTEGER_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_FLOAT_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_INT_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_OFFSET_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_ZONED_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.FORMATTED_CUSTOM_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.FORMATTED_CUSTOM_DATE_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.CustomScalars.FORMATTED_CUSTOM_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.ElementGenerator.OPEN_SQUARE; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getDefaultDateTimeFormat; +import static io.helidon.microprofile.graphql.server.SchemaGenerator.GET; +import static io.helidon.microprofile.graphql.server.SchemaGenerator.IS; +import static io.helidon.microprofile.graphql.server.SchemaGenerator.SET; + +/** + * Helper class for {@link SchemaGenerator}. + */ +final class SchemaGeneratorHelper { + + /** + * {@link OffsetTime} class name. + */ + protected static final String OFFSET_TIME_CLASS = OffsetTime.class.getName(); + + /** + * {@link LocalTime} class name. + */ + protected static final String LOCAL_TIME_CLASS = LocalTime.class.getName(); + + /** + * {@link OffsetDateTime} class name. + */ + protected static final String OFFSET_DATE_TIME_CLASS = OffsetDateTime.class.getName(); + + /** + * {@link ZonedDateTime} class name. + */ + protected static final String ZONED_DATE_TIME_CLASS = ZonedDateTime.class.getName(); + + /** + * {@link LocalDateTime} class name. + */ + protected static final String LOCAL_DATE_TIME_CLASS = LocalDateTime.class.getName(); + + /** + * {@link LocalDate} class name. + */ + protected static final String LOCAL_DATE_CLASS = LocalDate.class.getName(); + + /** + * {@link BigDecimal} class name. + */ + protected static final String BIG_DECIMAL_CLASS = BigDecimal.class.getName(); + + /** + * {@link Long} class name. + */ + protected static final String LONG_CLASS = Long.class.getName(); + + /** + * Class name for long primitive. + */ + protected static final String LONG_PRIMITIVE_CLASS = long.class.getName(); + + /** + * {@link Float} class name. + */ + protected static final String FLOAT_CLASS = Float.class.getName(); + + /** + * Class name for float primitive. + */ + protected static final String FLOAT_PRIMITIVE_CLASS = float.class.getName(); + + /** + * {@link Double} class name. + */ + protected static final String DOUBLE_CLASS = Double.class.getName(); + + /** + * Class name for double primitive. + */ + protected static final String DOUBLE_PRIMITIVE_CLASS = double.class.getName(); + + /** + * Class name for {@link BigInteger}. + */ + protected static final String BIG_INTEGER_CLASS = BigInteger.class.getName(); + + /** + * Class name for {@link Integer}. + */ + protected static final String INTEGER_CLASS = Integer.class.getName(); + + /** + * Class name for int. + */ + protected static final String INTEGER_PRIMITIVE_CLASS = int.class.getName(); + + /** + * Class name for {@link Byte}. + */ + protected static final String BYTE_CLASS = Byte.class.getName(); + + /** + * Class name for byte. + */ + protected static final String BYTE_PRIMITIVE_CLASS = byte.class.getName(); + + /** + * Class name for {@link Short}. + */ + protected static final String SHORT_CLASS = Short.class.getName(); + + /** + * Class name for short. + */ + protected static final String SHORT_PRIMITIVE_CLASS = short.class.getName(); + + /** + * Formatted Date scalar. + */ + public static final String FORMATTED_DATE_SCALAR = "FormattedDate"; + + /** + * Formatted DateTime scalar. + */ + public static final String FORMATTED_DATETIME_SCALAR = "FormattedDateTime"; + + /** + * Formatted DateTime scalar. + */ + public static final String FORMATTED_OFFSET_DATETIME_SCALAR = "FormattedOffsetDateTime"; + + /** + * Formatted DateTime scalar. + */ + public static final String FORMATTED_ZONED_DATETIME_SCALAR = "FormattedZonedDateTime"; + + /** + * Formatted Time Scalar. + */ + public static final String FORMATTED_TIME_SCALAR = "FormattedTime"; + + /** + * Formatted Int. + */ + + /** + * Date scalar (with default formatting). + */ + public static final String DATE_SCALAR = "Date"; + + /** + * DateTime scalar (with default formatting). + */ + public static final String DATETIME_SCALAR = "DateTime"; + + /** + * Time Scalar (with default formatting). + */ + public static final String TIME_SCALAR = "Time"; + + /** + * Defines a {@link BigDecimal} type. + */ + static final String BIG_DECIMAL = "BigDecimal"; + + /** + * Defines a {@link BigInteger} type. + */ + static final String BIG_INTEGER = "BigInteger"; + + /** + * Value that indicates that default {@link java.util.Locale}. + */ + static final String DEFAULT_LOCALE = "##default"; + + /** + * GraphQL Int. + */ + public static final String INT = "Int"; + + /** + * GraphQL Float. + */ + public static final String FLOAT = "Float"; + + /** + * GraphQL String. + */ + public static final String STRING = "String"; + + /** + * GraphQL ID. + */ + public static final String ID = "ID"; + + /** + * GraphQL Boolean. + */ + public static final String BOOLEAN = "Boolean"; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SchemaGeneratorHelper.class.getName()); + + /** + * Indicates empty annotations. + */ + private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; + + /** + * List of supported scalars keyed by the full class name. + */ + static final Map SUPPORTED_SCALARS = new HashMap<>() {{ + // Object Scalar + put(Object.class.getName(), new SchemaScalar("Object", Object.class.getName(), new ObjectScalar(), null)); + + // Time scalars + put(OffsetTime.class.getName(), + new SchemaScalar(FORMATTED_TIME_SCALAR, OFFSET_TIME_CLASS, FORMATTED_CUSTOM_TIME_SCALAR, "HH[:mm][:ss]Z")); + put(LocalTime.class.getName(), + new SchemaScalar(FORMATTED_TIME_SCALAR, LOCAL_TIME_CLASS, FORMATTED_CUSTOM_TIME_SCALAR, "HH[:mm][:ss]")); + + // DateTime scalars + put(OFFSET_DATE_TIME_CLASS, + new SchemaScalar(FORMATTED_OFFSET_DATETIME_SCALAR, OFFSET_DATE_TIME_CLASS, CUSTOM_OFFSET_DATE_TIME_SCALAR, + "yyyy-MM-dd'T'HH[:mm][:ss]Z")); + put(ZONED_DATE_TIME_CLASS, + new SchemaScalar(FORMATTED_ZONED_DATETIME_SCALAR, ZONED_DATE_TIME_CLASS, CUSTOM_ZONED_DATE_TIME_SCALAR, + "yyyy-MM-dd'T'HH[:mm][:ss]Z'['VV']'")); + put(LOCAL_DATE_TIME_CLASS, + new SchemaScalar(FORMATTED_DATETIME_SCALAR, LOCAL_DATE_TIME_CLASS, FORMATTED_CUSTOM_DATE_TIME_SCALAR, + "yyyy-MM-dd'T'HH[:mm][:ss]")); + + // Date scalar + put(LOCAL_DATE_CLASS, new SchemaScalar(FORMATTED_DATE_SCALAR, LOCAL_DATE_CLASS, FORMATTED_CUSTOM_DATE_SCALAR, + "yyyy-MM-dd")); + + // BigDecimal scalars + put(BIG_DECIMAL_CLASS, new SchemaScalar(BIG_DECIMAL, BIG_DECIMAL_CLASS, CUSTOM_BIGDECIMAL_SCALAR, null)); + + // BigInteger scalars + put(BIG_INTEGER_CLASS, new SchemaScalar(BIG_INTEGER, BIG_INTEGER_CLASS, CUSTOM_BIGINTEGER_SCALAR, null)); + put(LONG_PRIMITIVE_CLASS, new SchemaScalar(BIG_INTEGER, LONG_CLASS, CUSTOM_BIGINTEGER_SCALAR, null)); + put(LONG_CLASS, new SchemaScalar(BIG_INTEGER, LONG_CLASS, CUSTOM_BIGINTEGER_SCALAR, null)); + + // Int scalars + put(INTEGER_CLASS, new SchemaScalar(INT, INTEGER_CLASS, CUSTOM_INT_SCALAR, null)); + put(INTEGER_PRIMITIVE_CLASS, new SchemaScalar(INT, INTEGER_PRIMITIVE_CLASS, CUSTOM_INT_SCALAR, null)); + put(BYTE_CLASS, new SchemaScalar(INT, BYTE_CLASS, CUSTOM_INT_SCALAR, null)); + put(BYTE_PRIMITIVE_CLASS, new SchemaScalar(INT, BYTE_PRIMITIVE_CLASS, CUSTOM_INT_SCALAR, null)); + put(SHORT_CLASS, new SchemaScalar(INT, SHORT_CLASS, CUSTOM_INT_SCALAR, null)); + put(SHORT_PRIMITIVE_CLASS, new SchemaScalar(INT, SHORT_PRIMITIVE_CLASS, CUSTOM_INT_SCALAR, null)); + + // Float scalars + put(FLOAT_CLASS, new SchemaScalar(FLOAT, FLOAT_CLASS, CUSTOM_FLOAT_SCALAR, null)); + put(FLOAT_PRIMITIVE_CLASS, new SchemaScalar(FLOAT, FLOAT_PRIMITIVE_CLASS, CUSTOM_FLOAT_SCALAR, null)); + put(DOUBLE_CLASS, new SchemaScalar(FLOAT, DOUBLE_CLASS, CUSTOM_FLOAT_SCALAR, null)); + put(DOUBLE_PRIMITIVE_CLASS, new SchemaScalar(FLOAT, DOUBLE_PRIMITIVE_CLASS, CUSTOM_FLOAT_SCALAR, null)); + }}; + + /** + * List of types that should map to a GraphQL Boolean. + */ + static final List BOOLEAN_LIST = new ArrayList<>() {{ + add("boolean"); + add("java.lang.Boolean"); + }}; + + /** + * List of types that should map to a GraphQL String. + */ + static final List STRING_LIST = new ArrayList<>() {{ + add("java.lang.String"); + add("java.lang.Character"); + add("char"); + }}; + + /** + * List of array primitive types and their array mapping. See https://docs.oracle.com/javase/6/docs/api/java/lang/Class + * .html#getName%28%29 + */ + static final Map PRIMITIVE_ARRAY_MAP = new HashMap<>() {{ + put("[Z", "boolean"); + put("[B", "byte"); + put("[C", "char"); + put("[D", "double"); + put("[F", "float"); + put("[I", "int"); + put("[J", "long"); + put("[S", "short"); + }}; + + /** + * List of all Java primitives. + */ + static final List JAVA_PRIMITIVE_TYPES = new ArrayList<>() {{ + add("byte"); + add("short"); + add("int"); + add("long"); + add("float"); + add("double"); + add("boolean"); + add("char"); + }}; + + /** + * Private no-args constructor. + */ + private SchemaGeneratorHelper() { + } + + /** + * Return the simple name from a given class as a String. This takes into account any annotations that may be present. + * + * @param className class name + * @return the simple class name + * @throws ClassNotFoundException if invalid class name + */ + protected static String getSimpleName(String className) + throws ClassNotFoundException { + return getSimpleName(className, false); + } + + /** + * Return true of the {@link Class} is a primitive or array of primitives. + * + * @param clazz {@link Class} to check + * @return true of the {@link Class} is a primitive or array of primitives. + */ + protected static boolean isPrimitive(Class clazz) { + return isPrimitive(clazz.getName()); + } + + /** + * Return true of the class name is a primitive or array of primitives. + * + * @param clazz class name to check + * @return true if the class name is a primitive or array of primitives. + */ + protected static boolean isPrimitive(String clazz) { + return JAVA_PRIMITIVE_TYPES.contains(clazz) || PRIMITIVE_ARRAY_MAP.containsValue(clazz); + } + + /** + * Return true of the class name is an array of primitives. + * + * @param clazz class name to check + * @return true true of the class name is an array of primitives. + */ + protected static boolean isPrimitiveArray(String clazz) { + return PRIMITIVE_ARRAY_MAP.containsValue(clazz); + } + + /** + * Return true of the class name is an array of primitives. + * + * @param clazz {@link Class} to check + * @return true true of the class name is an array of primitives. + */ + protected static boolean isPrimitiveArray(Class clazz) { + String className = clazz.getName(); + for (String key : PRIMITIVE_ARRAY_MAP.keySet()) { + if (className.contains(key)) { + return true; + } + } + return false; + } + + /** + * Return the simple name from a given class as a String. This takes into account any annotations that may be present. + * + * @param className class name + * @param ignoreInputNameAnnotation indicates if we should ignore the name from {@link Input} annotation as we should not + * change the name of a type if it as and {@link Input} annotation + * @return the simple class name + * @throws ClassNotFoundException if invalid class name + */ + protected static String getSimpleName(String className, boolean ignoreInputNameAnnotation) + throws ClassNotFoundException { + if (ID.equals(className) + || STRING.equals(className) || BOOLEAN.equalsIgnoreCase(className) + || getScalar(className) != null) { + return className; + } + // return the type name taking into account any annotations + Class clazz = Class.forName(className); + return getTypeName(clazz, ignoreInputNameAnnotation); + } + + /** + * Return a {@link SchemaScalar} if one matches the known list of scalars available from the {@link ExtendedScalars} helper. + * + * @param clazzName class name to check for + * @return a {@link SchemaScalar} if one matches the known list of scalars or null if none found + */ + protected static SchemaScalar getScalar(String clazzName) { + return SUPPORTED_SCALARS.get(clazzName); + } + + /** + * Return true if the give name is a scalar with that name. + * + * @param scalarName the scalae name to check + * @return true if the give name is a scalar with that name + */ + protected static boolean isScalar(String scalarName) { + return SUPPORTED_SCALARS.values().stream().anyMatch((s -> s.name().equals(scalarName))); + } + + /** + * Return the GraphQL type for the given Java type. + * + * @param className fully qualified class name + * @return the GraphQL type + */ + protected static String getGraphQLType(String className) { + if (BOOLEAN_LIST.contains(className)) { + return BOOLEAN; + } else if (STRING_LIST.contains(className)) { + return STRING; + } else if (java.util.UUID.class.getName().equals(className)) { + return ID; + } + + return className; + } + + /** + * Return true of the name is a Date, DateTime, or Time scalar. + * + * @param scalarName scalar name + * @return true of the name is a Date, DateTime, or Time scalar + */ + protected static boolean isDateTimeScalar(String scalarName) { + return FORMATTED_DATE_SCALAR.equals(scalarName) + || FORMATTED_TIME_SCALAR.equals(scalarName) + || FORMATTED_OFFSET_DATETIME_SCALAR.equals(scalarName) + || FORMATTED_ZONED_DATETIME_SCALAR.equals(scalarName) + || FORMATTED_DATETIME_SCALAR.equals(scalarName); + } + + /** + * Return true if the type type is a native GraphQLType. + * + * @param type the type to check + * @return true if the type type is a GraphQLType + */ + protected static boolean isGraphQLType(String type) { + return BOOLEAN.equals(type) || STRING.equals(type) || ID.equals(type); + } + + /** + * Return the field name after checking both the {@link Name} and {@link JsonbProperty} annotations are present on the field + * name.

Name will take precedence if both are specified. + * + * @param clazz {@link Class} to check + * @param fieldName field name to check + * @return the field name or null if none exist + */ + protected static String getFieldName(Class clazz, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + Name nameAnnotation = field.getAnnotation(Name.class); + // Name annotation is specified so use this and don't bother checking JsonbProperty + if (nameAnnotation != null && !nameAnnotation.value().isBlank()) { + return nameAnnotation.value(); + } + // check for JsonbProperty + JsonbProperty jsonbPropertyAnnotation = field.getAnnotation(JsonbProperty.class); + return jsonbPropertyAnnotation != null && !jsonbPropertyAnnotation.value().isBlank() + ? jsonbPropertyAnnotation.value() + : null; + + } catch (NoSuchFieldException e) { + return null; + } + } + + /** + * Return the field name after checking both the {@link Name} and {@link JsonbProperty} annotations are present on the {@link + * Method}. Name will take precedence if both are specified. + * + * @param method {@link Method} to check + * @return the field name or null if non exist + */ + protected static String getMethodName(Method method) { + if (method != null) { + Query queryAnnotation = method.getAnnotation(Query.class); + Mutation mutationAnnotation = method.getAnnotation(Mutation.class); + Name nameAnnotation = method.getAnnotation(Name.class); + JsonbProperty jsonbPropertyAnnotation = method.getAnnotation(JsonbProperty.class); + if (queryAnnotation != null && !queryAnnotation.value().isBlank()) { + return queryAnnotation.value(); + } + if (mutationAnnotation != null && !mutationAnnotation.value().isBlank()) { + return mutationAnnotation.value(); + } + if (nameAnnotation != null && !nameAnnotation.value().isBlank()) { + // Name annotation is specified so use this and don't bother checking JsonbProperty + return nameAnnotation.value(); + } + if (jsonbPropertyAnnotation != null && !jsonbPropertyAnnotation.value().isBlank()) { + return jsonbPropertyAnnotation.value(); + } + } + return null; + } + + /** + * Return a Class from a class name and ignore any exceptions. + * + * @param clazzName the class name as a String + * @return a Class name + */ + protected static Class getSafeClass(String clazzName) { + try { + return Class.forName(clazzName); + } catch (ClassNotFoundException e) { + return null; + } + } + + /** + * Return true if the class is a date, time or date/time. + * + * @param clazz the {@link Class} to check + * @return true if the class is a date, time or date/time + */ + protected static boolean isDateTimeClass(Class clazz) { + return clazz != null && ( + clazz.equals(LocalDate.class) + || clazz.equals(LocalTime.class) + || clazz.equals(LocalDateTime.class) + || clazz.equals(OffsetTime.class) + || clazz.equals(ZonedDateTime.class) + || clazz.equals(OffsetDateTime.class)); + } + + /** + * Return true if the {@link Class} is a valid {@link Class} to apply the {@link Id} annotation. + * + * @param clazz {@link Class} to check + * @return true if the {@link Class} is a valid {@link Class} to apply the {@link Id} annotation + */ + protected static boolean isValidIDType(Class clazz) { + return clazz.equals(Long.class) || clazz.equals(Integer.class) + || clazz.equals(java.util.UUID.class) || clazz.equals(int.class) + || clazz.equals(String.class) || clazz.equals(long.class); + } + + /** + * Return true if the fully qualified class is an enum. + * + * @param clazz class to check + * @return true if the fully qualified class is an enum. + */ + protected static boolean isEnumClass(String clazz) { + try { + return (Class.forName(clazz)).isEnum(); + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Return the {@link Name} annotation value if it exists or null. + * + * @param clazz {@link Class} to check + * @return the {@link Name} annotation value if it exists or null + */ + protected static String getNameAnnotationValue(Class clazz) { + Name nameAnnotation = clazz.getAnnotation(Name.class); + if (nameAnnotation != null && !"".equals(nameAnnotation.value())) { + return nameAnnotation.value(); + } + return null; + } + + /** + * Return the {@link DefaultValue} annotation value if it exists for a {@link Parameter} or null. + * + * @param annotatedElement {@link AnnotatedElement} to check + * @return the {@link DefaultValue} annotation value if it exists or null + */ + protected static String getDefaultValueAnnotationValue(AnnotatedElement annotatedElement) { + DefaultValue defaultValueAnnotation = annotatedElement.getAnnotation(DefaultValue.class); + if (defaultValueAnnotation != null && !"".equals(defaultValueAnnotation.value())) { + return defaultValueAnnotation.value(); + } + return null; + } + + /** + * Return the default description based upon the format and description. + * + * @param format format + * @param description description + * @return the default description + */ + protected static String getDefaultDescription(String[] format, String description) { + String fmt = format == null || format.length != 2 || (format[0] == null || format[1] == null) + ? null : format[0] + (DEFAULT_LOCALE.equals(format[1]) ? "" : " " + format[1]); + if (description == null && fmt == null) { + return null; + } + + // for the format display replace all [] (optionals) with null so TCK works + if (fmt != null) { + fmt = fmt.replaceAll("\\[", "").replaceAll("]", ""); + } + return description == null + ? fmt.trim() : fmt == null + ? description : description + " (" + fmt.trim() + ")"; + } + + /** + * Return the type method name taking into account the is/set/get prefix. + * + * @param method {@link Method} to check + * @param isStrictTest indicates if a strict test for setters and getters should be carried out. + * @return the method name + */ + protected static String stripMethodName(Method method, boolean isStrictTest) { + String name = method.getName(); + boolean isPublic = Modifier.isPublic(method.getModifiers()); + boolean isSetterName = name.matches("^set[A-Z].*"); + boolean isGetterName = name.matches("^get[A-Z].*") || name.matches("^is[A-Z].*"); + Parameter[] parameters = method.getParameters(); + + if (isStrictTest) { + boolean isSetter = isPublic + && method.getReturnType().equals(void.class) + && parameters.length == 1 + && isSetterName; + boolean isGetter = isPublic + && !method.getReturnType().equals(void.class) + && parameters.length == 0 + && isGetterName; + + if (!isGetter && !isSetter) { + return name; + } + } else { + // non-strict test so just check names + if (!isPublic || !isGetterName && !isSetterName) { + return name; + } + } + + String varName; + if (name.startsWith(IS) || name.startsWith(GET) || name.startsWith(SET)) { + String prefix; + if (name.startsWith(IS)) { + prefix = IS; + } else if (name.startsWith(GET)) { + prefix = GET; + } else if (name.startsWith(SET)) { + prefix = SET; + } else { + prefix = ""; + } + + // remove the prefix and make first letter lowercase + varName = name.replaceAll(prefix, ""); + varName = varName.substring(0, 1).toLowerCase() + varName.substring(1); + } else { + // may be any method, e.g. from GraphQLApi annotated class + varName = name; + } + + return varName; + } + + /** + * Return correct name for a Type or Enum based upon the value of the annotation or the {@link Name}. + * + * @param clazz {@link Class} to introspect. + * @return the correct name + */ + protected static String getTypeName(Class clazz) { + return getTypeName(clazz, false); + } + + /** + * Return the array of {@link Annotation}s on a {@link Parameter} that are parameterized types. + * + * @param field {@link Field} to introspect + * @param index index of type generic type. 0 = List/Collection 1 = Map + * @return the array of {@link Annotation}s on a {@link Parameter} + */ + protected static Annotation[] getFieldAnnotations(Field field, int index) { + if (field.getAnnotatedType() instanceof AnnotatedParameterizedType) { + return getAnnotationsFromType((AnnotatedParameterizedType) field.getAnnotatedType(), index); + } + + return EMPTY_ANNOTATIONS; + } + + /** + * Return the array of {@link Annotation}s on a {@link Method} that are parameterized types. + * + * @param method {@link Method} to introspect + * @param index index of type generic type. 0 = List/Collection 1 = Map + * @return the array of {@link Annotation}s on a {@link Parameter} + */ + protected static Annotation[] getMethodAnnotations(Method method, int index) { + if (method.getAnnotatedReturnType() instanceof AnnotatedParameterizedType) { + return getAnnotationsFromType((AnnotatedParameterizedType) method.getAnnotatedReturnType(), index); + } + + return EMPTY_ANNOTATIONS; + } + + /** + * Return the array of {@link Annotation}s on a {@link Parameter} that are parameterized types. + * + * @param parameter {@link Parameter} to introspect + * @param index index of type generic type. 0 = List/Collection 1 = Map + * @return the array of {@link Annotation}s on a {@link Parameter} + */ + protected static Annotation[] getParameterAnnotations(Parameter parameter, int index) { + + if (parameter.getAnnotatedType() instanceof AnnotatedParameterizedType) { + return getAnnotationsFromType((AnnotatedParameterizedType) parameter.getAnnotatedType(), index); + } + + return EMPTY_ANNOTATIONS; + } + + /** + * Return the annotations from the given {@link AnnotatedParameterizedType}. + * + * @param apt {@link AnnotatedParameterizedType} + * @param index index of type generic type. 0 = List/Collection 1 = Map + * @return the annotations from the given {@link AnnotatedParameterizedType} + */ + private static Annotation[] getAnnotationsFromType(AnnotatedParameterizedType apt, int index) { + if (apt != null) { + + // loop until we find the root annotated type + AnnotatedType annotatedActualTypeArgument = apt.getAnnotatedActualTypeArguments()[index]; + while (annotatedActualTypeArgument instanceof AnnotatedParameterizedType) { + AnnotatedParameterizedType parameterizedType2 = (AnnotatedParameterizedType) annotatedActualTypeArgument; + annotatedActualTypeArgument = parameterizedType2.getAnnotatedActualTypeArguments()[index]; + } + + if (annotatedActualTypeArgument != null) { + return annotatedActualTypeArgument.getAnnotations(); + } + } + return EMPTY_ANNOTATIONS; + } + + /** + * Return the annotation that matches the type. + * + * @param annotations array of {@link Annotation}s to search + * @param type the {@link Type} to find + * @return the annotation that matches the type + */ + protected static Annotation getAnnotationValue(Annotation[] annotations, java.lang.reflect.Type type) { + if (annotations != null) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(type)) { + return annotation; + } + } + } + return null; + } + + /** + * Return correct name for a Type or Enum based upon the value of the annotation or the {@link Name}. + * + * @param clazz {@link Class} to introspect. + * @param ignoreInputNameAnnotation indicates if we should ignore the name from {@link Input} annotation as we should not + * change the name of a type if it as and {@link Input} annotation + * @return the correct name + */ + protected static String getTypeName(Class clazz, boolean ignoreInputNameAnnotation) { + Type typeAnnotation = clazz.getAnnotation(Type.class); + Interface interfaceAnnotation = clazz.getAnnotation(Interface.class); + Input inputAnnotation = ignoreInputNameAnnotation ? null : clazz.getAnnotation(Input.class); + Enum enumAnnotation = clazz.getAnnotation(Enum.class); + Query queryAnnotation = clazz.getAnnotation(Query.class); + Mutation mutationAnnotation = clazz.getAnnotation(Mutation.class); + + String name = ""; + if (typeAnnotation != null) { + name = typeAnnotation.value(); + } else if (interfaceAnnotation != null) { + name = interfaceAnnotation.value(); + } else if (inputAnnotation != null) { + name = inputAnnotation.value(); + } else if (enumAnnotation != null) { + name = enumAnnotation.value(); + } else if (queryAnnotation != null) { + name = queryAnnotation.value(); + } else if (mutationAnnotation != null) { + name = mutationAnnotation.value(); + } + + if ("".equals(name)) { + name = getNameAnnotationValue(clazz); + } + + ensureValidName(LOGGER, name); + + return name == null || name.isBlank() ? clazz.getSimpleName() : name; + } + + /** + * Ensure the provided name is a valid GraphQL name. + * + * @param logger {@link Logger} to log to + * @param name to validate + */ + protected static void ensureValidName(Logger logger, String name) { + if (name != null && !isValidGraphQLName(name)) { + ensureConfigurationException(LOGGER, "The name '" + name + "' is not a valid " + + "GraphQL name and cannot be used."); + } + } + + /** + * Checks a {@link SchemaType} for {@link SchemaFieldDefinition}s which contain known scalars and replace them with the scalar + * name. + * + * @param schema {@link Schema} to check scalars for. + * @param type {@link SchemaType} to check + */ + protected static void checkScalars(Schema schema, SchemaType type) { + type.fieldDefinitions().forEach(fd -> { + SchemaScalar scalar = getScalar(fd.returnType()); + if (scalar != null) { + fd.returnType(scalar.name()); + if (!schema.containsScalarWithName(scalar.name())) { + schema.addScalar(scalar); + } + } + }); + } + + /** + * Return current format or if none exists, then the default if it exists for the scalar. + * + * @param scalarName scalar name to check + * @param clazzName class name to check + * @param existingFormat the existing format + * @return current format or if none exists, then the default if it exists for the scalar + */ + protected static String[] ensureFormat(String scalarName, String clazzName, String[] existingFormat) { + if (existingFormat == null || (existingFormat[0] == null && existingFormat[1] == null && isScalar(scalarName))) { + String[] defaultFormat = getDefaultDateTimeFormat(scalarName, clazzName); + if (defaultFormat != null) { + return defaultFormat; + } + } + return existingFormat; + } + + /** + * Return the number of array levels in the class. + * + * @param clazz the class name retrieved via Class.getName() + * @return the number of array levels in the class + */ + protected static int getArrayLevels(String clazz) { + int c = 0; + for (int i = 0; i < clazz.length(); i++) { + if (clazz.charAt(i) == '[') { + c++; + } + } + return c; + } + + /** + * Indicates if the class is an array type. + * + * @param clazz the class name retrieved via Class.getName() + * @return true if the class is an array type + */ + protected static boolean isArrayType(String clazz) { + return clazz.startsWith(OPEN_SQUARE); + } + + /** + * Return the root array class from the given class. + * + * @param clazz the class name retrieved via Class.getName() + * @return the root class name + */ + protected static String getRootArrayClass(String clazz) { + if (clazz == null || "".equals(clazz.trim()) || clazz.length() < 2) { + throw new IllegalArgumentException("Class must be not null"); + } + // check to see if it is a primitive array + String type = PRIMITIVE_ARRAY_MAP.get(clazz.substring(clazz.length() - 2)); + if (type != null) { + return type; + } + // must be an object + return clazz.replaceAll("\\[", "").replaceAll(";", "").replaceAll("^L", ""); + } + + /** + * Indicates if the method should be ignored. + * + * @param method {@link Method} to check + * @param isInputType indicates if this is an input type + * @return true if the method should be ignored + */ + protected static boolean shouldIgnoreMethod(Method method, boolean isInputType) { + Ignore ignore = method.getAnnotation(Ignore.class); + JsonbTransient jsonbTransient = method.getAnnotation(JsonbTransient.class); + + // default case + if (ignore == null && jsonbTransient == null) { + return false; + } + + // at least one of the annotations is present on the method + String methodName = method.getName(); + String prefix = methodName.startsWith(SET) + ? SET : methodName.startsWith(GET) + ? GET + : null; + + // if @Ignore or @JsonbTransient is on getter method then exclude from output type + // if @Ignore or @JsonbTransient is on setter methods then excludes from input type + if (GET.equals(prefix) && !isInputType) { + return true; + } else { + return SET.equals(prefix) && isInputType; + } + } + + /** + * Return true if the provided field should be ignored. + * + * @param clazz {@link Class} to check field on + * @param fieldName field name to check + * @return true if the {@link Field} should be ignored + */ + protected static boolean shouldIgnoreField(Class clazz, String fieldName) { + Field field = null; + try { + field = clazz.getDeclaredField(fieldName); + return field != null + && (field.getAnnotation(Ignore.class) != null || field.getAnnotation(JsonbTransient.class) != null); + } catch (NoSuchFieldException e) { + return false; + } + } + + /** + * Safely return the value of the {@link Description} annotation. + * + * @param description {@link Description} annotation + * @return the description or null + */ + protected static String getDescription(Description description) { + return description == null || "".equals(description.value()) + ? null + : description.value(); + } + + /** + * Validates that a name is valid according to the graphql spec at. Ref: http://spec.graphql.org/June2018/#sec-Names + * + * @param name name to validate + * @return true if the name is valid + */ + protected static boolean isValidGraphQLName(String name) { + return name != null && name.matches("[_A-Za-z][_0-9A-Za-z]*") && !name.startsWith("__"); + } + + /** + * Ensures a {@link RuntimeException} with the message supplied is thrown and logged. + * + * @param message message to throw + * @param logger the {@link Logger} to use + */ + protected static void ensureRuntimeException(Logger logger, String message) { + ensureRuntimeException(logger, message, null); + } + + /** + * Ensures a {@link RuntimeException} with the message supplied is thrown and logged. + * + * @param message message to throw + * @param cause cause of the erro + * @param logger the {@link Logger} to use + */ + protected static void ensureRuntimeException(Logger logger, String message, Throwable cause) { + logger.warning(message); + if (cause != null) { + logger.warning(getStackTrace(cause)); + } + throw new RuntimeException(message, cause); + } + + /** + * Ensures a {@link GraphQlConfigurationException} with the message suppleid is thrown and logged. + * + * @param message message to throw + * @param logger the {@link Logger} to use + */ + protected static void ensureConfigurationException(Logger logger, String message) { + ensureConfigurationException(logger, message, null); + } + + /** + * Ensures a {@link GraphQlConfigurationException} with the message supplied is thrown and logged. + * + * @param message message to throw + * @param cause cause of the erro + * @param logger the {@link Logger} to use + */ + protected static void ensureConfigurationException(Logger logger, String message, Throwable cause) { + logger.warning(message); + if (cause != null) { + logger.warning(getStackTrace(cause)); + } + throw new GraphQlConfigurationException(message, cause); + } + + /** + * Return the stacktrace of the given {@link Throwable}. + * + * @param throwable {@link Throwable} to get stack tracek for + * @return the stacktrace of the given {@link Throwable} + */ + protected static String getStackTrace(Throwable throwable) { + StringWriter stack = new StringWriter(); + throwable.printStackTrace(new PrintWriter(stack)); + return stack.toString(); + } + + /** + * Validate that a {@link Class} annotated with ID is a valid type. + * + * @param returnClazz {@link Class} to check + */ + protected static void validateIDClass(Class returnClazz) { + if (!isValidIDType(returnClazz)) { + ensureConfigurationException(LOGGER, "A class of type " + returnClazz + " is not allowed to be an @Id"); + } + } + + /** + * Validate that a class annotated with ID is a valid type. + * + * @param returnClazz class to check + */ + protected static void validateIDClass(String returnClazz) { + try { + validateIDClass(Class.forName(returnClazz)); + } catch (ClassNotFoundException e) { + // ignore + } + } + + /** + * Defines discovered methods for a class. + */ + public static class DiscoveredMethod { + + /** + * Indicates query Type. + */ + public static final int QUERY_TYPE = 0; + + /** + * Indicates write method. + */ + public static final int MUTATION_TYPE = 1; + + /** + * Name of the discovered method. + */ + private String name; + + /** + * Return type of the method. + */ + private String returnType; + + /** + * type of method. + */ + private int methodType; + + /** + * If the return type is a {@link Collection} then this is the type of {@link Collection} and the returnType will be + * return type for the collection. + */ + private String collectionType; + + /** + * Indicates if the return type is an array. + */ + private boolean isArrayReturnType; + + /** + * Indicates if the return type is a {@link Map}. Note: In the 1.0.1 microprofile spec the behaviour of {@link Map} is + * undefined. + */ + private boolean isMap; + + /** + * The {@link List} of {@link SchemaArgument}s for this method. + */ + private List listArguments = new ArrayList<>(); + + /** + * The actual method. + */ + private Method method; + + /** + * Number of levels in the Array. + */ + private int arrayLevels = 0; + + /** + * The source on which the method should be added. + */ + private String source; + + /** + * The property name if the method is a getter. + */ + private String propertyName; + + /** + * Indicates if the method containing the {@link Source} annotation was also annotated with the {@link Query} annotation. + * If true, then this indicates that a top level query should also be created as well as the field in the type. + */ + private boolean isQueryAnnotated = false; + + /** + * Defines the format for a number or date. + */ + private String[] format = new String[0]; + + /** + * A description for a method. + */ + private String description; + + /** + * Indicates id the return type is mandatory. + */ + private boolean isReturnTypeMandatory; + + /** + * The default value for this discovered method. + */ + private Object defaultValue; + + /** + * If the return type is an array then indicates if the value in the array is mandatory. + */ + private boolean isArrayReturnTypeMandatory; + + /** + * Original array inner type if it is array type. + */ + private Class originalArrayType; + + /** + * Indicates if the format is of type Jsonb. + */ + private boolean isJsonbFormat; + + /** + * Indicates if the property name is of type Jsonb. + */ + private boolean isJsonbProperty; + + /** + * Construct a {@link DiscoveredMethod}. + * + * @param builder the {@link Builder} to construct from + */ + private DiscoveredMethod(Builder builder) { + this.name = builder.name; + this.returnType = builder.returnType; + this.methodType = builder.methodType; + this.collectionType = builder.collectionType; + this.isArrayReturnType = builder.isArrayReturnType; + this.isMap = builder.isMap; + this.listArguments = builder.listArguments; + this.method = builder.method; + this.arrayLevels = builder.arrayLevels; + this.source = builder.source; + this.propertyName = builder.propertyName; + this.isQueryAnnotated = builder.isQueryAnnotated; + this.format = builder.format; + this.description = builder.description; + this.isArrayReturnTypeMandatory = builder.isReturnTypeMandatory; + this.defaultValue = builder.defaultValue; + this.originalArrayType = builder.originalArrayType; + this.isJsonbFormat = builder.isJsonbFormat; + this.isJsonbProperty = builder.isJsonbProperty; + } + + /** + * Fluent API builder to create {@link DiscoveredMethod}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the name. + * + * @return the name + */ + public String name() { + return name; + } + + /** + * Set the name. + * + * @param name the name + */ + public void name(String name) { + this.name = name; + } + + /** + * Return the return type. + * + * @return the return type + */ + public String returnType() { + return returnType; + } + + /** + * Set the return type. + * + * @param returnType the return type + */ + public void returnType(String returnType) { + this.returnType = returnType; + } + + /** + * Return the collection type. + * + * @return the collection + */ + public String collectionType() { + return collectionType; + } + + /** + * Set the collection type. + * + * @param collectionType the collection type + */ + public void collectionType(String collectionType) { + this.collectionType = collectionType; + } + + /** + * Sets if the property has a JsonbProperty annotation. + * + * @param isJsonbProperty if the property has a JsonbProperty annotation + */ + public void jsonbProperty(boolean isJsonbProperty) { + this.isJsonbProperty = isJsonbProperty; + } + + /** + * Indicates if the property has a JsonbProperty annotation. + * + * @return true if the property has a JsonbProperty annotation + */ + public boolean isJsonbProperty() { + return isJsonbProperty; + } + + /** + * Indicates if the method is a map return type. + * + * @return if the method is a map return type + */ + public boolean isMap() { + return isMap; + } + + /** + * Set if the method is a map return type. + * + * @param map if the method is a map return type + */ + public void map(boolean map) { + isMap = map; + } + + /** + * Return the method type. + * + * @return the method type + */ + public int methodType() { + return methodType; + } + + /** + * Set the method type either READ or WRITE. + * + * @param methodType the method type + */ + public void methodType(int methodType) { + this.methodType = methodType; + } + + /** + * Indicates if the method returns an array. + * + * @return if the method returns an array. + */ + public boolean isArrayReturnType() { + return isArrayReturnType; + } + + /** + * Indicates if the method returns an array. + * + * @param arrayReturnType if the method returns an array + */ + public void arrayReturnType(boolean arrayReturnType) { + isArrayReturnType = arrayReturnType; + } + + /** + * Indicates if the method is a collection type. + * + * @return if the method is a collection type + */ + public boolean isCollectionType() { + return collectionType != null; + } + + /** + * Return the {@link Method}. + * + * @return the {@link Method} + */ + public Method method() { + return method; + } + + /** + * Sets the {@link Method}. + * + * @param method the {@link Method} + */ + public void method(Method method) { + this.method = method; + } + + /** + * Return the {@link List} of {@link SchemaArgument}s. + * + * @return the {@link List} of {@link SchemaArgument} + */ + public List arguments() { + return this.listArguments; + } + + /** + * Return the number of levels in the Array. + * + * @return Return the number of levels in the Array + */ + public int arrayLevels() { + return arrayLevels; + } + + /** + * Sets the number of levels in the Array. + * + * @param arrayLevels the number of levels in the Array + */ + public void arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + } + + /** + * Add a {@link SchemaArgument}. + * + * @param argument a {@link SchemaArgument} + */ + public void addArgument(SchemaArgument argument) { + listArguments.add(argument); + } + + /** + * Return the source on which the method should be added. + * + * @return source on which the method should be added + */ + public String source() { + return source; + } + + /** + * Set the source on which the method should be added. + * + * @param source source on which the method should be added + */ + public void source(String source) { + this.source = source; + } + + /** + * Indicates if the method containing the {@link Source} annotation was also annotated with the {@link Query} annotation. + * + * @return true if the {@link Query} annotation was present + */ + public boolean isQueryAnnotated() { + return isQueryAnnotated; + } + + /** + * Set if the method containing the {@link Source} annotation was * also annotated with the {@link Query} annotation. + * + * @param queryAnnotated true if the {@link Query} annotation was present + */ + public void queryAnnotated(boolean queryAnnotated) { + isQueryAnnotated = queryAnnotated; + } + + /** + * Return the format for a number or date. + * + * @return the format for a number or date + */ + public String[] format() { + if (format == null) { + return null; + } + String[] copy = new String[format.length]; + System.arraycopy(format, 0, copy, 0, copy.length); + return copy; + } + + /** + * Set the format for a number or date. + * + * @param format the format for a number or date + */ + public void format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + } + + /** + * Return the property name if the method is a getter. + * + * @return property name if the method is a getter + */ + public String propertyName() { + return propertyName; + } + + /** + * Set the property name if the method is a getter. + * + * @param propertyName property name + */ + public void propertyName(String propertyName) { + this.propertyName = propertyName; + } + + /** + * Return the description for a method. + * + * @return the description for a method + */ + public String description() { + return description; + } + + /** + * Set the description for a method. + * + * @param description the description for a method + */ + public void description(String description) { + this.description = description; + } + + /** + * Indicates if the return type is mandatory. + * + * @return if the return type is mandatory + */ + public boolean isReturnTypeMandatory() { + return isReturnTypeMandatory; + } + + /** + * Set if the return type is mandatory. + * + * @param returnTypeMandatory if the return type is mandatory + */ + public void returnTypeMandatory(boolean returnTypeMandatory) { + isReturnTypeMandatory = returnTypeMandatory; + } + + /** + * Return the default value for this method. + * + * @return the default value for this method + */ + public Object defaultValue() { + return defaultValue; + } + + /** + * Set the default value for this method. + * + * @param defaultValue the default value for this method + */ + public void defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Return if the array return type is mandatory. + * + * @return if the array return type is mandatory + */ + public boolean isArrayReturnTypeMandatory() { + return isArrayReturnTypeMandatory; + } + + /** + * Sets if the array return type is mandatory. + * + * @param arrayReturnTypeMandatory if the array return type is mandatory + */ + public void arrayReturnTypeMandatory(boolean arrayReturnTypeMandatory) { + isArrayReturnTypeMandatory = arrayReturnTypeMandatory; + } + + /** + * Sets the original array type. + * + * @param originalArrayType the original array type + */ + public void originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + } + + /** + * Return the original array type. + * + * @return the original array type + */ + public Class originalArrayType() { + return originalArrayType; + } + + /** + * Set if the format is of type JsonB. + * + * @param isJsonbFormat if the format is of type JsonB + */ + public void jsonbFormat(boolean isJsonbFormat) { + this.isJsonbFormat = isJsonbFormat; + } + + /** + * Return true if the format is of type JsonB. + * + * @return true if the format is of type JsonB + */ + public boolean isJsonbFormat() { + return isJsonbFormat; + } + + @Override + public String toString() { + return "DiscoveredMethod{" + + "name='" + name + '\'' + + ", returnType='" + returnType + '\'' + + ", methodType=" + methodType + + ", collectionType='" + collectionType + '\'' + + ", isArrayReturnType=" + isArrayReturnType + + ", isMap=" + isMap + + ", listArguments=" + listArguments + + ", arrayLevels=" + arrayLevels + + ", source=" + source + + ", isQueryAnnotated=" + isQueryAnnotated + + ", isReturnTypeMandatory=" + isReturnTypeMandatory + + ", isArrayReturnTypeMandatory=" + isArrayReturnTypeMandatory + + ", description=" + description + + ", originalArrayType=" + originalArrayType + + ", defaultValue=" + defaultValue + + ", isJsonbFormat=" + isJsonbFormat + + ", isJsonbProperty=" + isJsonbProperty + + ", method=" + method + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DiscoveredMethod that = (DiscoveredMethod) o; + return methodType == that.methodType + && isArrayReturnType == that.isArrayReturnType + && isMap == that.isMap + && arrayLevels == that.arrayLevels + && Objects.equals(name, that.name) + && Objects.equals(returnType, that.returnType) + && Objects.equals(source, that.source) + && Objects.equals(isQueryAnnotated, that.isQueryAnnotated) + && Objects.equals(method, that.method) + && Objects.equals(description, that.description) + && Objects.equals(isReturnTypeMandatory, that.isReturnTypeMandatory) + && Objects.equals(isArrayReturnTypeMandatory, that.isArrayReturnTypeMandatory) + && Objects.equals(defaultValue, that.defaultValue) + && Objects.equals(isJsonbFormat, that.isJsonbFormat) + && Objects.equals(collectionType, that.collectionType); + } + + @Override + public int hashCode() { + return Objects.hash(name, returnType, methodType, method, arrayLevels, isQueryAnnotated, + collectionType, isArrayReturnType, isMap, source, description, + isReturnTypeMandatory, defaultValue, isArrayReturnTypeMandatory, isJsonbFormat, + isJsonbProperty); + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link DiscoveredMethod}. + */ + public static class Builder implements io.helidon.common.Builder { + private String name; + private String returnType; + private int methodType; + private String collectionType; + private boolean isArrayReturnType; + private boolean isMap; + private List listArguments = new ArrayList<>(); + private Method method; + private int arrayLevels = 0; + private String source; + private String propertyName; + private boolean isQueryAnnotated = false; + private String[] format = new String[0]; + private String description; + private boolean isReturnTypeMandatory; + private Object defaultValue; + private Class originalArrayType; + private boolean isJsonbFormat; + private boolean isJsonbProperty; + + /** + * Set the name of the {@link DiscoveredMethod}. + * + * @param name the name of the {@link DiscoveredMethod} + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the returnType. + * + * @param returnType the returnType + * @return updated builder instance + */ + public Builder returnType(String returnType) { + this.returnType = returnType; + return this; + } + + /** + * Set the method type. + * + * @param methodType the method type + * @return updated builder instance + */ + public Builder methodType(int methodType) { + this.methodType = methodType; + return this; + } + + /** + * Set the collection type. + * + * @param collectionType the collection type + * @return updated builder instance + */ + public Builder collectionType(String collectionType) { + this.collectionType = collectionType; + return this; + } + + /** + * Set if the return type is an array type such as a native array([]) or a List, Collection. + * + * @param isArrayReturnType true if the return type is an array type + * @return updated builder instance + */ + public Builder arrayReturnType(boolean isArrayReturnType) { + this.isArrayReturnType = isArrayReturnType; + return this; + } + + /** + * Indicates if the return type is a {@link Map}. + * @param isMap if the return type is a {@link Map} + * @return updated builder instance + */ + public Builder map(boolean isMap) { + this.isMap = isMap; + return this; + } + + /** + * Add an argument to the {@link DiscoveredMethod}. + * + * @param argument the argument to add to the {@link DiscoveredMethod} + * @return updated builder instance + */ + public Builder addArgument(SchemaArgument argument) { + listArguments.add(argument); + return this; + } + + /** + * Set the actual method. + * @param method the actual method + * @return updated builder instance + */ + public Builder method(Method method) { + this.method = method; + return this; + } + + /** + * Set the number of array levels if return type is an array. + * + * @param arrayLevels the number of array levels if return type is an array + * @return updated builder instance + */ + public Builder arrayLevels(int arrayLevels) { + this.arrayLevels = arrayLevels; + return this; + } + + /** + * Set the source on which the method should be added. + * @param source the source on which the method should be added + * @return updated builder instance + */ + public Builder source(String source) { + this.source = source; + return this; + } + + /** + * The property name if this property is a getter. + * @param propertyName property name if this property is a getter + * @return updated builder instance + */ + public Builder propertyName(String propertyName) { + this.propertyName = propertyName; + return this; + } + + /** + * Indicates if the method containing the {@link Source} annotation was also annotated with the {@link Query} + * annotation. + * + * @param isQueryAnnotated if the method containing the {@link Source} annotation was also annotated + * @return updated builder instance + */ + public Builder queryAnnotated(boolean isQueryAnnotated) { + this.isQueryAnnotated = isQueryAnnotated; + return this; + } + + /** + * Set the format for a number or date. + * + * @param format the format for a number or date + * @return updated builder instance + */ + public Builder format(String[] format) { + if (format == null) { + this.format = null; + } else { + this.format = new String[format.length]; + System.arraycopy(format, 0, this.format, 0, this.format.length); + } + + return this; + } + + /** + * Set the description. + * + * @param description the description of the {@link DiscoveredMethod} + * @return updated builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Set if the return type is mandatory. + * + * @param isReturnTypeMandatory true if the return type is mandatory. + * @return updated builder instance + */ + public Builder returnTypeMandatory(boolean isReturnTypeMandatory) { + this.isReturnTypeMandatory = isReturnTypeMandatory; + return this; + } + + /** + * Set the default value for this {@link DiscoveredMethod}. + * + * @param defaultValue the default value for this field definition + * @return updated builder instance + */ + public Builder defaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Set the original array inner type if it is array type. + * + * @param originalArrayType the original array inner type if it is array type + * @return updated builder instance + */ + public Builder originalArrayType(Class originalArrayType) { + this.originalArrayType = originalArrayType; + return this; + } + + /** + * Set if the format is of type Jsonb. + * + * @param isJsonbFormat if the format is of type Jsonb. + * @return updated builder instance + */ + public Builder jsonbFormat(boolean isJsonbFormat) { + this.isJsonbFormat = isJsonbFormat; + return this; + } + + /** + * Set if the property name is of type Jsonb. + * + * @param isJsonbProperty if the property name is of type Jsonb. + * @return updated builder instance + */ + public Builder jsonbProperty(boolean isJsonbProperty) { + this.isJsonbProperty = isJsonbProperty; + return this; + } + + @Override + public DiscoveredMethod build() { + return new DiscoveredMethod(this); + } + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaInputType.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaInputType.java new file mode 100644 index 00000000000..9b326c63162 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaInputType.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +/** + * The representation of a GraphQL Input Type. + */ +class SchemaInputType extends SchemaType { + + /** + * Private no-args constructor. + * @param name name of the type + * @param valueClassName value class name + */ + protected SchemaInputType(String name, String valueClassName) { + super(name, valueClassName); + } + + @Override + public void addFieldDefinition(SchemaFieldDefinition schemaFieldDefinition) { + if (schemaFieldDefinition.arguments().size() > 0) { + throw new IllegalArgumentException("Input types cannot have fields with arguments"); + } + super.addFieldDefinition(schemaFieldDefinition); + } + + @Override + protected String getGraphQLName() { + return "input"; + } + + @Override + public String toString() { + return "InputType" + toStringInternal(); + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaScalar.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaScalar.java new file mode 100644 index 00000000000..a819a397a0f --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaScalar.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.Objects; + +import graphql.schema.GraphQLScalarType; + +/** + * The representation of a GraphQL Scalar. + */ +class SchemaScalar implements ElementGenerator { + + /** + * Name of the Scalar. + */ + private String name; + + /** + * Actual class name. + */ + private String actualClass; + + /** + * {@link GraphQLScalarType} to convert this {@link SchemaScalar}. + */ + private GraphQLScalarType graphQLScalarType; + + /** + * The default format if none is specified. + */ + private String defaultFormat; + + /** + * Construct a {@link SchemaScalar}. + * + * @param name name + * @param actualClass actual class name + * @param graphQLScalarType {@link GraphQLScalarType} to convert this {@link SchemaScalar}. + * @param defaultFormat default format or null if none + */ + SchemaScalar(String name, String actualClass, GraphQLScalarType graphQLScalarType, String defaultFormat) { + this.name = name; + this.actualClass = actualClass; + this.graphQLScalarType = graphQLScalarType; + this.defaultFormat = defaultFormat; + } + + /** + * Return the name of the {@link SchemaScalar}. + * + * @return the name of the {@link SchemaScalar} + */ + String name() { + return name; + } + + /** + * Return the actual class name of the {@link SchemaScalar}. + * + * @return the actual class name of the {@link SchemaScalar} + */ + String actualClass() { + return actualClass; + } + + /** + * Return the {@link GraphQLScalarType} instance. + * + * @return the {@link GraphQLScalarType} instance. + */ + GraphQLScalarType graphQLScalarType() { + return graphQLScalarType; + } + + /** + * The default format if none is specified. + * + * @return default format if none is specified + */ + String defaultFormat() { + return defaultFormat; + } + + /** + * Set the default format if none is specified. + * @param defaultFormat default format if none is specified + */ + void defaultFormat(String defaultFormat) { + this.defaultFormat = defaultFormat; + } + + @Override + public String getSchemaAsString() { + return "scalar " + name(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SchemaScalar schemaScalar = (SchemaScalar) o; + return Objects.equals(name, schemaScalar.name) + && Objects.equals(actualClass, schemaScalar.actualClass) + && Objects.equals(defaultFormat, schemaScalar.defaultFormat) + && Objects.equals(graphQLScalarType, schemaScalar.graphQLScalarType); + } + + @Override + public int hashCode() { + return Objects.hash(name, actualClass, graphQLScalarType, defaultFormat); + } + + @Override + public String toString() { + return "Scalar{" + + "name='" + name + '\'' + + ", actualClass='" + actualClass + '\'' + + ", defaultFormat='" + defaultFormat + '\'' + + ", graphQLScalarType=" + graphQLScalarType + '}'; + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaType.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaType.java new file mode 100644 index 00000000000..b902cfeb43a --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/SchemaType.java @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * The representation of a GraphQL Type. + */ +class SchemaType extends AbstractDescriptiveElement implements ElementGenerator { + + /** + * Name of the type. + */ + private String name; + + /** + * Value class name. + */ + private String valueClassName; + + /** + * Indicates if the {@link SchemaType} is an interface. + */ + private boolean isInterface; + + /** + * The interface that this {@link SchemaType} implements. + */ + private String implementingInterface; + + /** + * {@link List} of {@link SchemaFieldDefinition}. + */ + private List listSchemaFieldDefinitions; + + /** + * Private no-args constructor only use by subclass {@link SchemaInputType}. + * @param name name of the type + * @param valueClassName value class name + */ + protected SchemaType(String name, String valueClassName) { + this.name = name; + this.valueClassName = valueClassName; + this.listSchemaFieldDefinitions = new ArrayList<>(); + } + + /** + * Construct a {@link SchemaType}. + * + * @param builder the {@link Builder} to construct from + */ + private SchemaType(Builder builder) { + this.name = builder.name; + this.valueClassName = builder.valueClassName; + this.isInterface = builder.isInterface; + this.implementingInterface = builder.implementingInterface; + this.listSchemaFieldDefinitions = builder.listSchemaFieldDefinitions; + description(builder.description); + } + + /** + * Fluent API builder to create {@link SchemaType}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return the GraphQL schema representation of the element. + * + * @return the GraphQL schema representation of the element. + */ + @Override + public String getSchemaAsString() { + if (listSchemaFieldDefinitions.size() == 0) { + // should not generate anything if no field definitions + return ""; + } + StringBuilder sb = new StringBuilder(getSchemaElementDescription(null)) + .append(getGraphQLName()) + .append(SPACER) + .append(name()); + + if (implementingInterface != null) { + sb.append(" implements ").append(implementingInterface); + } + sb.append(SPACER).append(OPEN_CURLY).append(NEWLINE); + + listSchemaFieldDefinitions.forEach(fd -> sb.append(fd.getSchemaAsString()).append(NEWLINE)); + + sb.append(CLOSE_CURLY).append(NEWLINE); + + return sb.toString(); + } + + /** + * Generates a {@link SchemaInputType} from the current {@link SchemaType}. + * + * @param sSuffix the suffix to add to the type as it must be unique within Type and InputType + * @return an new {@link SchemaInputType} + */ + public SchemaInputType createInputType(String sSuffix) { + SchemaInputType inputType = new SchemaInputType(name() + sSuffix, valueClassName()); + fieldDefinitions().forEach(fd -> { + fd.arguments().clear(); + inputType.addFieldDefinition(fd); + }); + return inputType; + } + + /** + * Set if the {@link SchemaType} is an interface. + * + * @param isInterface indicates if the {@link SchemaType} is an interface; + */ + public void isInterface(boolean isInterface) { + this.isInterface = isInterface; + } + + /** + * Return the name of the {@link SchemaType}. + * + * @return the name of the {@link SchemaType} + */ + public String name() { + return name; + } + + /** + * Set the name of the {@link SchemaType}. + * + * @param name the name of the {@link SchemaType} + */ + public void name(String name) { + this.name = name; + } + + /** + * Return the value class name for the @{link Type}. + * + * @return the value class name for the @{link Type}. + */ + public String valueClassName() { + return valueClassName; + } + + /** + * Return the {@link List} of {@link SchemaFieldDefinition}s. + * + * @return the {@link List} of {@link SchemaFieldDefinition}s + */ + public List fieldDefinitions() { + return listSchemaFieldDefinitions; + } + + /** + * Indicates if the {@link SchemaType} is an interface. + * + * @return if the {@link SchemaType} is an interface. + */ + public boolean isInterface() { + return isInterface; + } + + /** + * Return the interface that this {@link SchemaType} implements. + * + * @return the interface that this {@link SchemaType} implements + */ + public String implementingInterface() { + return implementingInterface; + } + + /** + * Set the interface that this {@link SchemaType} implements. + * + * @param implementingInterface the interface that this {@link SchemaType} implements + */ + public void implementingInterface(String implementingInterface) { + this.implementingInterface = implementingInterface; + } + + /** + * Add a {@link SchemaFieldDefinition} to the {@link SchemaType}. + * + * @param schemaFieldDefinition {@link SchemaFieldDefinition} + */ + public void addFieldDefinition(SchemaFieldDefinition schemaFieldDefinition) { + listSchemaFieldDefinitions.add(schemaFieldDefinition); + } + + /** + * Return true if there is one or more {@link SchemaFieldDefinition}s. + * @return true if there is one or more {@link SchemaFieldDefinition}s + */ + public boolean hasFieldDefinitions() { + return listSchemaFieldDefinitions != null && listSchemaFieldDefinitions.size() > 0; + } + + /** + * Return a {@link SchemaFieldDefinition} that matches the name. + * + * @param fdName type name to match + * @return a {@link SchemaType} that matches the type name or null if none found + */ + public SchemaFieldDefinition getFieldDefinitionByName(String fdName) { + for (SchemaFieldDefinition fieldDefinition : listSchemaFieldDefinitions) { + if (fieldDefinition.name().equals(fdName)) { + return fieldDefinition; + } + } + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SchemaType schemaType = (SchemaType) o; + return isInterface == schemaType.isInterface + && Objects.equals(name, schemaType.name) + && Objects.equals(valueClassName, schemaType.valueClassName) + && Objects.equals(implementingInterface, schemaType.implementingInterface) + && Objects.equals(listSchemaFieldDefinitions, schemaType.listSchemaFieldDefinitions) + && Objects.equals(description(), schemaType.description()); + } + + @Override + public int hashCode() { + return Objects.hash(name, + valueClassName, + isInterface, + implementingInterface, + description(), + listSchemaFieldDefinitions); + + } + + @Override + public String toString() { + return "Type" + toStringInternal(); + } + + /** + * Internal toString() used by sub-type. + * + * @return internal toString() + */ + protected String toStringInternal() { + return "{" + + "name='" + name + '\'' + + ", valueClassName='" + valueClassName + '\'' + + ", isInterface='" + isInterface + '\'' + + ", description='" + description() + '\'' + + ", implementingInterface='" + implementingInterface + '\'' + + ", listFieldDefinitions=" + listSchemaFieldDefinitions + '}'; + } + + /** + * Return the GraphQL name for this {@link SchemaType}. + * + * @return the GraphQL name for this {@link SchemaType}. + */ + protected String getGraphQLName() { + return isInterface() ? "interface" : "type"; + } + + /** + * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaType}. + */ + public static class Builder implements io.helidon.common.Builder { + + private String name; + private String valueClassName; + private String description; + private boolean isInterface; + private String implementingInterface; + private List listSchemaFieldDefinitions = new ArrayList<>(); + + /** + * Set the name of the {@link SchemaType}. + * + * @param name the name of the {@link SchemaType} + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Set the the value class name. + * @param valueClassName the value class name + * @return updated builder instance + */ + public Builder valueClassName(String valueClassName) { + this.valueClassName = valueClassName; + return this; + } + + /** + * Set the description of the {@link SchemaType}. + * @param description the description of the {@link SchemaType} + * @return updated builder instance + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Set if the {@link SchemaType} is an interface. + * @param isInterface true if the {@link SchemaType} is an interface + * @return updated builder instance + */ + public Builder isInterface(boolean isInterface) { + this.isInterface = isInterface; + return this; + } + + /** + * Set the interface that this {@link SchemaType} implements. + * @param implementingInterface the interface that this {@link SchemaType} implements + * @return updated builder instance + */ + public Builder implementingInterface(String implementingInterface) { + this.implementingInterface = implementingInterface; + return this; + } + + /** + * Add a {@link SchemaFieldDefinition} to the {@link SchemaType}. + * @param fieldDefinition {@link SchemaFieldDefinition} to add + * @return updated builder instance + */ + public Builder addFieldDefinition(SchemaFieldDefinition fieldDefinition) { + this.listSchemaFieldDefinitions.add(fieldDefinition); + return this; + } + + @Override + public SchemaType build() { + Objects.requireNonNull(name, "Name must be specified"); + return new SchemaType(this); + } + } +} diff --git a/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/package-info.java b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/package-info.java new file mode 100644 index 00000000000..9cf05435718 --- /dev/null +++ b/microprofile/graphql/server/src/main/java/io/helidon/microprofile/graphql/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Microprofile GraphQL server implementation. + */ +package io.helidon.microprofile.graphql.server; diff --git a/microprofile/graphql/server/src/main/java/module-info.java b/microprofile/graphql/server/src/main/java/module-info.java new file mode 100644 index 00000000000..0ce38ce2a4f --- /dev/null +++ b/microprofile/graphql/server/src/main/java/module-info.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.microprofile.graphql.server.GraphQlCdiExtension; + +/** + * GraphQL microprofile server module. + */ +module helidon.microprofile.graphql.server { + requires java.logging; + requires java.desktop; + + requires java.json.bind; + requires java.annotation; + requires jakarta.enterprise.cdi.api; + requires org.eclipse.yasson; + + requires jandex; + + requires io.helidon.config; + requires io.helidon.webserver; + requires io.helidon.graphql.server; + requires io.helidon.microprofile.cdi; + requires io.helidon.microprofile.server; + + requires graphql.java; + requires graphql.java.extended.scalars; + requires microprofile.graphql.api; + requires microprofile.config.api; + + exports io.helidon.microprofile.graphql.server; + + provides javax.enterprise.inject.spi.Extension with + GraphQlCdiExtension; + + opens io.helidon.microprofile.graphql.server to weld.core.impl; +} diff --git a/microprofile/graphql/server/src/main/resources/META-INF/helidon/native-image/reflection-config.json b/microprofile/graphql/server/src/main/resources/META-INF/helidon/native-image/reflection-config.json new file mode 100644 index 00000000000..091aee3d61b --- /dev/null +++ b/microprofile/graphql/server/src/main/resources/META-INF/helidon/native-image/reflection-config.json @@ -0,0 +1,14 @@ +{ + "annotated":[ + "org.eclipse.microprofile.graphql.GraphQLApi", + "org.eclipse.microprofile.graphql.Input", + "org.eclipse.microprofile.graphql.Interface", + "org.eclipse.microprofile.graphql.Type" + ], + "class-hierarchy": [ + ], + "classes": [ + ], + "exclude": [ + ] +} diff --git a/microprofile/graphql/server/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/microprofile/graphql/server/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..ebe120a2283 --- /dev/null +++ b/microprofile/graphql/server/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.microprofile.graphql.server.GraphQlCdiExtension diff --git a/microprofile/graphql/server/src/main/resources/web/index.html b/microprofile/graphql/server/src/main/resources/web/index.html new file mode 100644 index 00000000000..f757724f133 --- /dev/null +++ b/microprofile/graphql/server/src/main/resources/web/index.html @@ -0,0 +1,48 @@ + + + + + + + Helidon Microprofile GraphiQL Interface + + + +

+ + + + + + + + \ No newline at end of file diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLTest.java new file mode 100644 index 00000000000..1dac13af8c0 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLTest.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import graphql.ExecutionResult; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaPrinter; +import org.hamcrest.CoreMatchers; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexWriter; +import org.jboss.jandex.Indexer; +import org.junit.jupiter.api.Assertions; + +import static io.helidon.graphql.server.GraphQlConstants.COLUMN; +import static io.helidon.graphql.server.GraphQlConstants.DATA; +import static io.helidon.graphql.server.GraphQlConstants.ERRORS; +import static io.helidon.graphql.server.GraphQlConstants.EXTENSIONS; +import static io.helidon.graphql.server.GraphQlConstants.LINE; +import static io.helidon.graphql.server.GraphQlConstants.LOCATIONS; +import static io.helidon.graphql.server.GraphQlConstants.MESSAGE; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Functionality for use by unit and functional tests. + * + * @author Tim Middleton 2020.02.28 + */ +public abstract class AbstractGraphQLTest { + private SchemaPrinter schemaPrinter; + + /** + * Create a Jandex index using the given file name and classes. + * + * @param fileName the file name to write the index to. The classes should be in the format of "java/lang/Thread.class" + * @param clazzes classes to index + */ + public static void createManualIndex(String fileName, String... clazzes) throws IOException { + Indexer indexer = new Indexer(); + for (String clazz : clazzes) { + InputStream stream = AbstractGraphQLTest.class.getClassLoader().getResourceAsStream(clazz); + indexer.index(stream); + stream.close(); + } + Index index = indexer.complete(); + + FileOutputStream out = new FileOutputStream(fileName); + IndexWriter writer = new IndexWriter(out); + try { + writer.write(index); + } finally { + out.close(); + } + } + + /** + * Return a temporary file which will be used to import the Jandex index to. + * + * @return a new {@link File} + * @throws IOException if any IO related errors + */ + public static String getTempIndexFile() throws IOException { + return Files.createTempFile("index" + System.currentTimeMillis(), "idx").toFile().toString(); + } + + /** + * Return true if the contents of the String match the contents of the file. + * + * @param results String to compare + * @param fileName filename to compare + */ + protected static void assertResultsMatch(String results, String fileName) { + if (results == null || fileName == null) { + throw new IllegalArgumentException("sResults or sFileName cannot be null"); + } + + try { + ClassLoader classLoader = AbstractGraphQLTest.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + if (resource == null) { + throw new IllegalArgumentException("Unable to find comparison file " + fileName); + } + File file = new File(resource.getFile()); + String sFromFile = new String(Files.readAllBytes(file.toPath())); + + assertThat("Results do not match expected", results, is(sFromFile)); + } catch (Exception e) { + throw new RuntimeException("Exception in resultsMatch sResults=[" + results + "], sFileName=" + fileName, e); + } + } + + protected GraphQLSchema generateGraphQLSchema(Schema schema) { + + try { + GraphQLSchema graphQLSchema = schema.generateGraphQLSchema(); + displaySchema(graphQLSchema); + return graphQLSchema; + } catch (Exception e) { + Assertions.fail("Schema generation failed. " + e.getMessage() + + "\ncause: " + e.getCause() + + "\nSchema: \n" + schema.getSchemaAsString()); + return null; + } + } + + protected void displaySchema(GraphQLSchema graphQLSchema) { + System.err.println("Schema:\n=======\n" + + getSchemaPrinter().print(graphQLSchema) + + "\n======="); + } + + protected SchemaPrinter getSchemaPrinter() { + if (schemaPrinter == null) { + SchemaPrinter.Options options = SchemaPrinter.Options + .defaultOptions().includeDirectives(false) + .includeScalarTypes(true); + schemaPrinter = new SchemaPrinter(options); + } + return schemaPrinter; + } + + /** + * Setup an index file for the given {@link Class}es. + * + * @param clazzes classes to setup index for + * @throws IOException + */ + protected static void setupIndex(String indexFileName, Class... clazzes) throws IOException { + createManualIndex(indexFileName, Arrays.stream(clazzes).map(c -> getIndexClassName(c)).toArray(String[]::new)); + System.setProperty(JandexUtils.PROP_INDEX_FILE, indexFileName); + assertThat(indexFileName, CoreMatchers.is(notNullValue())); + File indexFile = new File(indexFileName); + assertThat(indexFile.exists(), CoreMatchers.is(true)); + + // do a load to check the classes are there + JandexUtils utils = JandexUtils.create(); + utils.loadIndexes(); + assertThat(utils.hasIndex(), CoreMatchers.is(true)); + int count = 0; + for (Index index : utils.getIndexes()) { + count += index.getKnownClasses().size(); + } + + assertThat(count, CoreMatchers.is(clazzes.length)); + } + + protected static String getIndexClassName(Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Must not be null"); + } + return clazz.getName().replaceAll("\\.", "/") + ".class"; + } + + @SuppressWarnings("unchecked") + protected String getError(List> result) { + assertThat(result, CoreMatchers.is(notNullValue())); + StringBuilder sb = new StringBuilder("Errors: "); + for (Map mapError : result) { + sb.append(mapError.get(MESSAGE)).append('\n'); + List> listLocations = (List>) mapError.get(LOCATIONS); + Map mapExtensions = (Map) mapError.get(EXTENSIONS); + + if (listLocations != null) { + for (Map mapLocations : listLocations) { + sb.append(LINE).append(':') + .append(mapLocations.get(LINE)) + .append(COLUMN).append(':') + .append(mapLocations.get(COLUMN)); + } + } + + if (mapExtensions != null) { + mapExtensions.entrySet().forEach((e) -> sb.append(e.getKey() + "=" + e.getValue())); + } + } + return sb.toString(); + } + + /** + * Assert an {@link ExecutionResult} is true and if not then display the error and fail. + * + * @param result {@link ExecutionResult} data + */ + @SuppressWarnings("unchecked") + protected Map getAndAssertResult(Map result) { + List> listErrors = (List>) result.get(ERRORS); + boolean failed = listErrors != null && listErrors.size() > 0; + if (failed) { + String sError = getError(listErrors); + fail(sError); + } + return (Map) result.get(DATA); + } + + protected String getContactAsQueryInput(SimpleContact contact) { + return new StringBuilder("{") + .append("id: \"").append(contact.getId()).append("\" ") + .append("name: \"").append(contact.getName()).append("\" ") + .append("age: ").append(contact.getAge()) + .append("} ").toString(); + } + + protected void assertDefaultFormat(SchemaType type, String fdName, String defaultFormat, boolean isDefaultFormatApplied) { + assertThat(type, CoreMatchers.is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + assertThat(fd, CoreMatchers.is(notNullValue())); + assertThat(fd.isDefaultFormatApplied(), is(isDefaultFormatApplied)); + String[] format = fd.format(); + assertThat(format, CoreMatchers.is(notNullValue())); + assertThat(format.length == 2, CoreMatchers.is(notNullValue())); + assertThat(format[0], CoreMatchers.is(defaultFormat)); + } + + protected SchemaFieldDefinition getFieldDefinition(SchemaType type, String name) { + for (SchemaFieldDefinition fd : type.fieldDefinitions()) { + if (fd.name().equals(name)) { + return fd; + } + } + return null; + } + + protected SchemaArgument getArgument(SchemaFieldDefinition fd, String name) { + assertThat(fd, CoreMatchers.is(notNullValue())); + for (SchemaArgument argument : fd.arguments()) { + if (argument.argumentName().equals(name)) { + return argument; + } + } + return null; + } + + protected void assertReturnTypeDefaultValue(SchemaType type, String fdName, String defaultValue) { + assertThat(type, CoreMatchers.is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + assertThat(fd, CoreMatchers.is(notNullValue())); + assertThat("Default value for " + fdName + " should be " + defaultValue + + " but is " + fd.defaultValue(), fd.defaultValue(), CoreMatchers.is(defaultValue)); + } + + protected void assertReturnTypeMandatory(SchemaType type, String fdName, boolean mandatory) { + assertThat(type, CoreMatchers.is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + assertThat(fd, CoreMatchers.is(notNullValue())); + assertThat("Return type for " + fdName + " should be mandatory=" + mandatory + + " but is " + fd.isReturnTypeMandatory(), fd.isReturnTypeMandatory(), CoreMatchers.is(mandatory)); + } + + protected void assertArrayReturnTypeMandatory(SchemaType type, String fdName, boolean mandatory) { + assertThat(type, CoreMatchers.is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + assertThat(fd, CoreMatchers.is(notNullValue())); + assertThat("Array return type for " + fdName + " should be mandatory=" + mandatory + + " but is " + fd.isArrayReturnTypeMandatory(), fd.isArrayReturnTypeMandatory(), CoreMatchers + .is(mandatory)); + } + + protected void assertReturnTypeArgumentMandatory(SchemaType type, String fdName, String argumentName, boolean mandatory) { + assertThat(type, CoreMatchers.is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + assertThat(fd, CoreMatchers.is(notNullValue())); + SchemaArgument argument = getArgument(fd, argumentName); + assertThat(argument, CoreMatchers.is(notNullValue())); + assertThat("Return type for argument " + argumentName + " should be mandatory=" + + mandatory + " but is " + argument.mandatory(), argument.mandatory(), CoreMatchers.is(mandatory)); + } + + protected SchemaGenerator createSchemaGenerator() { + return SchemaGenerator.builder() + .build(); + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JandexUtilsTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JandexUtilsTest.java new file mode 100644 index 00000000000..61ccc3db1d5 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JandexUtilsTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for {@link JandexUtils}. + */ +class JandexUtilsTest extends AbstractGraphQLTest { + + @BeforeEach + public void resetProperties() { + System.clearProperty(JandexUtils.PROP_INDEX_FILE); + } + + @Test + public void testDefaultIndexFile() { + JandexUtils utils = JandexUtils.create(); + assertThat(utils.getIndexFile(), is(JandexUtils.DEFAULT_INDEX_FILE)); + assertThat(utils.hasIndex(), is(false)); + } + + @Test + public void testCustomIndexFile() { + System.setProperty(JandexUtils.PROP_INDEX_FILE, "my-index-file"); + JandexUtils utils = JandexUtils.create(); + assertThat(utils.getIndexFile(), is("my-index-file")); + } + + @Test + public void testLoadingCustomIndexFile() throws IOException, ClassNotFoundException { + String indexFile = getTempIndexFile(); + try { + System.setProperty(JandexUtils.PROP_INDEX_FILE, indexFile); + createManualIndex(indexFile, + "java/lang/String.class", + "java/lang/Double.class", + "java/lang/Integer.class"); + JandexUtils utils = JandexUtils.create(); + utils.loadIndexes(); + + assertThat(utils.hasIndex(), is(true)); + Set setIndexes = utils.getIndexes(); + + for (Index index : setIndexes) { + for (Class c : new Class[] { String.class, Double.class, Integer.class }) { + ClassInfo classByName = index.getClassByName(DotName.createSimple(c.getName())); + assertThat(classByName, is(notNullValue())); + assertThat(classByName.toString(), is(c.getName())); + } + } + } finally { + if (indexFile != null) { + new File(indexFile).delete(); + } + } + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JsonUtilsTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JsonUtilsTest.java new file mode 100644 index 00000000000..da6a2800bd9 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/JsonUtilsTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link JandexUtils}. + */ +class JsonUtilsTest extends AbstractGraphQLTest { + + @Test + @SuppressWarnings("unchecked") + public void testValidJSON() { + Map jsonMap = JsonUtils.convertJSONtoMap("{\"name\": \"tim\" }"); + assertThat(jsonMap, is(CoreMatchers.notNullValue())); + assertThat(jsonMap.size(), is(1)); + assertThat(jsonMap.get("name"), is("tim")); + + jsonMap = JsonUtils.convertJSONtoMap("{\"name\": \"tim\", \"address\": { \"address1\": \"address line 1\", \"city\": \"Perth\" } }"); + assertThat(jsonMap, is(CoreMatchers.notNullValue())); + assertThat(jsonMap.size(), is(2)); + assertThat(jsonMap.get("name"), is("tim")); + + Map mapAddress = (Map) jsonMap.get("address"); + assertThat(mapAddress, is(CoreMatchers.notNullValue())); + assertThat(mapAddress.size(), is(2)); + + assertThat(mapAddress.get("address1"), is("address line 1")); + assertThat(mapAddress.get("city"), is("Perth")); + } + + @Test + public void testNullJson() { + Map jsonMap = JsonUtils.convertJSONtoMap(null); + assertThat(jsonMap.size(), is(0)); + } + + @Test + public void testEmptyJson() { + Map jsonMap = JsonUtils.convertJSONtoMap(" "); + assertThat(jsonMap.size(), is(0)); + } + + @Test + public void testConvertToJson() { + Map map = new HashMap<>(); + map.put("name", "tim"); + assertThat(JsonUtils.convertMapToJson(map), is("{\"name\":\"tim\"}")); + } + + @Test + public void testGraphQLSDLGeneration() { + + assertThat(JsonUtils.convertJsonToGraphQLSDL(10), is("10")); + assertThat(JsonUtils.convertJsonToGraphQLSDL("hello"), is("\"hello\"")); + + Map map = new TreeMap<>(); + map.put("id", "ID-1"); + map.put("value", BigDecimal.valueOf(100)); + assertThat(JsonUtils.convertJsonToGraphQLSDL(map), is("{ id: \"ID-1\", value: 100}")); + + map.clear(); + map.put("name", "This contains a quote \""); + assertThat(JsonUtils.convertJsonToGraphQLSDL(map), is("{ name: \"This contains a quote \\\"\"}")); + + map.clear(); + map.put("field1", "key"); + map.put("field2", Arrays.asList("one", "two", "three")); + assertThat(JsonUtils.convertJsonToGraphQLSDL(map), is("{ field1: \"key\", field2: [\"one\", \"two\", \"three\"]}")); + + String input = "{" + + " \"id\": 1000," + + " \"name\": \"Cape\","+ + " \"powerLevel\": 3," + + " \"height\": 1.2," + + " \"weight\": 0.3," + + " \"supernatural\": false," + + " \"dateCreated\": \"19 February 1900 at 12:00 in Africa/Johannesburg\"," + + " \"dateLastUsed\": \"29 Jan 2020 at 09:45 in zone +0200\"" + + "}"; + + String output = "{ id: 1000, name: \"Cape\", powerLevel: 3, height: 1.2, weight: 0.3, supernatural: false, " + + "dateCreated: \"19 February 1900 at 12:00 in Africa/Johannesburg\", dateLastUsed: \"29 Jan 2020 at 09:45 in " + + "zone +0200\"}"; + + map = JsonUtils.convertJSONtoMap(input); + assertThat(map.size(), is(8)); + assertThat(JsonUtils.convertJsonToGraphQLSDL(map), is(output)); + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaArgumentTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaArgumentTest.java new file mode 100644 index 00000000000..e29820b006a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaArgumentTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link SchemaArgument} class. + */ +class SchemaArgumentTest { + + private static final Class STRING = String.class; + private static final Class INTEGER = Integer.class; + + @Test + public void testBuilders() { + SchemaArgument schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(true) + .defaultValue(null) + .originalType(INTEGER) + .build(); + + assertThat(schemaArgument.argumentName(), is("name")); + assertThat(schemaArgument.argumentType(), is("Int")); + assertThat(schemaArgument.mandatory(), is(true)); + assertThat(schemaArgument.defaultValue(), is(nullValue())); + assertThat(schemaArgument.originalType().getName(), is(Integer.class.getName())); + + schemaArgument = SchemaArgument.builder() + .argumentName("name2") + .argumentType("String") + .mandatory(false) + .defaultValue("Default") + .originalType(STRING) + .build(); + + assertThat(schemaArgument.argumentName(), is("name2")); + assertThat(schemaArgument.argumentType(), is("String")); + assertThat(schemaArgument.mandatory(), is(false)); + assertThat(schemaArgument.defaultValue(), is("Default")); + assertThat(schemaArgument.originalType().getName(), is(STRING.getName())); + + schemaArgument.argumentType("XYZ"); + assertThat(schemaArgument.argumentType(), is("XYZ")); + + assertThat(schemaArgument.description(), is(nullValue())); + schemaArgument.description("description"); + assertThat(schemaArgument.description(), is("description")); + + assertThat(schemaArgument.isSourceArgument(), is(false)); + schemaArgument.sourceArgument(true); + assertThat(schemaArgument.isSourceArgument(), is(true)); + + assertThat(schemaArgument.format(), is(nullValue())); + schemaArgument.format(new String[] { "value-1", "value-2" }); + String[] format = schemaArgument.format(); + assertThat(format, is(notNullValue())); + assertThat(format.length, is(2)); + assertThat(format[0], is("value-1")); + assertThat(format[1], is("value-2")); + + schemaArgument.defaultValue("hello"); + assertThat(schemaArgument.defaultValue(), is("hello")); + + schemaArgument.defaultValue("1"); + assertThat(schemaArgument.defaultValue(), is("1")); + + assertThat(schemaArgument.originalArrayType(), is(nullValue())); + schemaArgument.originalArrayType(String.class); + assertThat(schemaArgument.originalArrayType().getName(), is(String.class.getName())); + } + + @Test + public void testSchemaArgumentArrayTypes() { + SchemaArgument schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(true) + .originalType(INTEGER) + .build(); + + assertThat(schemaArgument.isArrayReturnType(), is(false)); + assertThat(schemaArgument.isArrayReturnTypeMandatory(), is(false)); + assertThat(schemaArgument.arrayLevels(), is(0)); + } + + @Test + public void testSchemaGenerationWithArrays() { + SchemaArgument schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("String") + .mandatory(false) + .originalType(STRING) + .arrayLevels(1) + .arrayReturnTypeMandatory(true) + .arrayReturnType(true) + .build(); + + assertThat(schemaArgument.getSchemaAsString(), is("name: [String!]")); + } + + @Test + public void testSchemaGeneration() { + SchemaArgument schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(true) + .defaultValue(null) + .originalType(INTEGER) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: Int!")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(true) + .defaultValue(10) + .originalType(INTEGER) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: Int! = 10")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(false) + .defaultValue(null) + .originalType(INTEGER) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: Int")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(false) + .defaultValue(10) + .originalType(INTEGER) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: Int = 10")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("String") + .mandatory(false) + .defaultValue("The Default Value") + .originalType(STRING) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: String = \"The Default Value\"")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("String") + .mandatory(true) + .defaultValue("The Default Value") + .originalType(STRING) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: String! = \"The Default Value\"")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(false) + .defaultValue(10) + .originalType(INTEGER) + .description("Description") + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("\"Description\"\nname: Int = 10")); + + // test array return types + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(false) + .defaultValue(null) + .originalType(STRING) + .arrayReturnType(true) + .arrayLevels(1) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: [Int]")); + + schemaArgument.arrayReturnTypeMandatory(true); + assertThat(schemaArgument.getSchemaAsString(), is("name: [Int!]")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("Int") + .mandatory(true) + .defaultValue(null) + .arrayReturnType(true) + .originalType(INTEGER) + .arrayLevels(1) + .arrayReturnTypeMandatory(true) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: [Int!]!")); + + schemaArgument = SchemaArgument.builder() + .argumentName("name") + .argumentType("String") + .mandatory(true) + .defaultValue("Hello") + .arrayReturnType(true) + .originalType(STRING) + .arrayLevels(3) + .arrayReturnTypeMandatory(true) + .build(); + assertThat(schemaArgument.getSchemaAsString(), is("name: [[[String!]]]! = \"Hello\"")); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaDirectiveTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaDirectiveTest.java new file mode 100644 index 00000000000..809627f07fb --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaDirectiveTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import org.junit.jupiter.api.Test; + +import static graphql.introspection.Introspection.DirectiveLocation.FIELD; +import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION; +import static graphql.introspection.Introspection.DirectiveLocation.QUERY; +import static io.helidon.microprofile.graphql.server.TestHelper.createArgument; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for {@link SchemaScalar} class. + */ +class SchemaDirectiveTest { + + private static final Class STRING = String.class; + private static final Class INTEGER = Integer.class; + + @Test + public void testConstructors() { + SchemaDirective schemaDirective = SchemaDirective.builder().name("auth").build(); + assertThat(schemaDirective.name(), is("auth")); + assertThat(schemaDirective.arguments().size(), is(0)); + assertThat(schemaDirective.locations().size(), is(0)); + + SchemaArgument arg = createArgument("name", "String", true, null, STRING); + schemaDirective.addArgument(arg); + assertThat(schemaDirective.arguments().contains(arg), is(true)); + + schemaDirective.addLocation(FIELD_DEFINITION.name()); + assertThat(schemaDirective.locations().contains(FIELD_DEFINITION.name()), is(true)); + + assertThat(schemaDirective.getSchemaAsString(), is(notNullValue())); + } + + @Test + public void testDirectiveWith0Argument1Location() { + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addLocation(FIELD_DEFINITION.name()).build(); + assertThat(schemaDirective.getSchemaAsString(), is("directive @directiveName on " + FIELD_DEFINITION.name())); + } + + @Test + public void testDirectiveWith1Argument1Location() { + SchemaArgument arg = createArgument("name", "String", true, null, STRING); + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addArgument(arg) + .addLocation(FIELD_DEFINITION.name()) + .build(); + assertThat(schemaDirective.getSchemaAsString(), is("directive @directiveName(name: String!) on " + FIELD_DEFINITION.name())); + } + + @Test + public void testDirectiveWith2Argument1Location() { + SchemaArgument arg1 = createArgument("name", "String", true, null, STRING); + SchemaArgument arg2 = createArgument("name1", "Int", false, null, INTEGER); + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addArgument(arg1) + .addArgument(arg2) + .addLocation(FIELD_DEFINITION.name()) + .build(); + assertThat(schemaDirective.getSchemaAsString(), + is("directive @directiveName(name: String!, name1: Int) on " + FIELD_DEFINITION.name())); + } + + @Test + public void testDirectiveWith2Argument2Location() { + SchemaArgument arg1 = createArgument("name", "String", true, null, STRING); + SchemaArgument arg2 = createArgument("name1", "Int", false, null,INTEGER); + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addArgument(arg1) + .addArgument(arg2) + .addLocation(FIELD_DEFINITION.name()) + .addLocation(FIELD.name()) + .build(); + + assertThat(schemaDirective.getSchemaAsString(), + is("directive @directiveName(name: String!, name1: Int) on " + FIELD_DEFINITION.name() + "|" + FIELD.name())); + } + + @Test + public void testDirectiveWith0Argument2Location() { + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addLocation(FIELD_DEFINITION.name()) + .addLocation(FIELD.name()) + .build(); + + assertThat(schemaDirective.getSchemaAsString(), + is("directive @directiveName on " + FIELD_DEFINITION.name() + "|" + FIELD.name())); + } + + @Test + public void testDirectiveWith0Argument3Location() { + SchemaDirective schemaDirective = SchemaDirective.builder() + .name("directiveName") + .addLocation(FIELD_DEFINITION.name()) + .addLocation(FIELD.name()) + .addLocation(QUERY.name()) + .build(); + assertThat(schemaDirective.getSchemaAsString(), + is("directive @directiveName on " + FIELD_DEFINITION.name() + "|" + FIELD.name() + "|" + QUERY.name())); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaEnumTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaEnumTest.java new file mode 100644 index 00000000000..7dcc7e6bd2e --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaEnumTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link SchemaEnum} class. + */ +class SchemaEnumTest extends AbstractGraphQLTest { + + @Test + public void testConstructor() { + + SchemaEnum schemaEnum1 = SchemaEnum.builder() + .name("ShirtSize") + .description("This is the description of the Enum") + .addValue("S") + .addValue("M") + .addValue("L") + .addValue("XL") + .addValue("XXL") + .addValue("3XL") + .build(); + + assertThat(schemaEnum1.description(), is("This is the description of the Enum")); + assertThat(schemaEnum1.values(), is(notNullValue())); + assertThat(schemaEnum1.values().size(), is(6)); + } + + @Test + public void testSchemaGenerationWithDescription() { + SchemaEnum schemaEnum1 = SchemaEnum.builder() + .name("ShirtSize") + .description("T Shirt Size") + .addValue("Small") + .addValue("Medium") + .addValue("Large") + .addValue("XLarge") + .build(); + + assertResultsMatch(schemaEnum1.getSchemaAsString(), "test-results/enum-test-01.txt"); + } + + @Test + public void testSchemaGenerationWithoutDescription() { + SchemaEnum schemaEnum1 = SchemaEnum.builder() + .name("ShirtSize") + .addValue("Small") + .addValue("Medium") + .addValue("Large") + .addValue("XLarge") + .build(); + + assertResultsMatch(schemaEnum1.getSchemaAsString(), "test-results/enum-test-02.txt"); + } + + @Test + public void testSchemaGenerationWithDescriptionAndQuote() { + SchemaEnum schemaEnum1 = SchemaEnum.builder() + .name("ShirtSize") + .addValue("Small") + .addValue("Medium") + .addValue("Large") + .addValue("XLarge") + .description("Description\"") + .build(); + + assertResultsMatch(schemaEnum1.getSchemaAsString(), "test-results/enum-test-03.txt"); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinitionTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinitionTest.java new file mode 100644 index 00000000000..e785f1a9d10 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaFieldDefinitionTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import graphql.schema.DataFetcher; +import graphql.schema.StaticDataFetcher; + +import org.junit.jupiter.api.Test; + +import static io.helidon.microprofile.graphql.server.TestHelper.createArgument; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests for {@link SchemaArgument} class. + */ +class SchemaFieldDefinitionTest { + + private static final Class STRING = String.class; + private static final Class INTEGER = Integer.class; + + @Test + public void testConstructors() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("name") + .returnType("Integer") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(1) + .build(); + + assertThat(schemaFieldDefinition.name(), is("name")); + assertThat(schemaFieldDefinition.returnType(), is("Integer")); + assertThat(schemaFieldDefinition.arguments(), is(notNullValue())); + assertThat(schemaFieldDefinition.isArrayReturnType(), is(true)); + assertThat(schemaFieldDefinition.arrayLevels(), is(1)); + + SchemaArgument schemaArgument = createArgument("filter", "String", false, null, STRING); + schemaFieldDefinition.addArgument(schemaArgument); + assertThat(schemaFieldDefinition.arguments().size(), is(1)); + assertThat(schemaFieldDefinition.arguments().get(0), is(schemaArgument)); + + SchemaArgument schemaArgument2 = createArgument("filter2", "Integer", true, null, INTEGER); + schemaFieldDefinition.addArgument(schemaArgument2); + assertThat(schemaFieldDefinition.arguments().size(), is(2)); + assertThat(schemaFieldDefinition.arguments().contains(schemaArgument2), is(true)); + + schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("name2") + .returnType("String") + .arrayReturnType(false) + .returnTypeMandatory(false) + .arrayLevels(0) + .build(); + assertThat(schemaFieldDefinition.name(), is("name2")); + assertThat(schemaFieldDefinition.returnType(), is("String")); + assertThat(schemaFieldDefinition.isArrayReturnType(), is(false)); + assertThat(schemaFieldDefinition.isReturnTypeMandatory(), is(false)); + assertThat(schemaFieldDefinition.arrayLevels(), is(0)); + + schemaFieldDefinition.returnType("BLAH"); + assertThat(schemaFieldDefinition.returnType(), is("BLAH")); + + assertThat(schemaFieldDefinition.format(), is(nullValue())); + schemaFieldDefinition.format(new String[] { "a", "b" }); + String[] format = schemaFieldDefinition.format(); + assertThat(format, is(notNullValue())); + assertThat(format.length, is(2)); + assertThat(format[0], is("a")); + assertThat(format[1], is("b")); + + assertThat(schemaFieldDefinition.isArrayReturnTypeMandatory(), is(false)); + schemaFieldDefinition.arrayReturnTypeMandatory(true); + assertThat(schemaFieldDefinition.isArrayReturnTypeMandatory(), is(true)); + + assertThat(schemaFieldDefinition.isDefaultFormatApplied(), is(false)); + schemaFieldDefinition.defaultFormatApplied(true); + assertThat(schemaFieldDefinition.isDefaultFormatApplied(), is(true)); + + assertThat(schemaFieldDefinition.isJsonbFormat(), is(false)); + schemaFieldDefinition.jsonbFormat(true); + assertThat(schemaFieldDefinition.isJsonbFormat(), is(true)); + } + + @Test + public void testFieldDefinitionWithNoArguments() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person: Person!")); + } + + @Test + public void testFieldDefinitionWithNoArgumentsAndDescription() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .description("Description") + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("\"Description\"\nperson: Person!")); + } + + @Test + public void testFieldDefinitionWithNoArgumentsAndArrayType1ArrayLevel() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(1) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person: [Person]!")); + } + + @Test + public void testFieldDefinitionWithNoArgumentsAndArrayType2ArrayLevels() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(2) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person: [[Person]]!")); + } + + @Test + public void testFieldDefinitionWithNoArgumentsAndArrayType3ArrayLevels() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("result") + .returnType("String") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(3) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("result: [[[String]]]!")); + } + + @Test + public void testFieldDefinitionWith1Argument() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\nfilter: String\n): Person!")); + } + + @Test + public void testFieldDefinitionWith1ArgumentAndDescription() { + SchemaArgument schemaArgument = createArgument("filter", "String", false, null, STRING); + schemaArgument.description("Optional Filter"); + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .addArgument(schemaArgument) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\n\"Optional Filter\"\nfilter: String\n): Person!")); + } + + @Test + public void testFieldDefinitionWith1ArgumentAndArrayType() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(1) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\nfilter: String\n): [Person]!")); + } + + @Test + public void testFieldDefinitionWithNoArgumentsAndMandatoryArrayType() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("superPowers") + .returnType("String") + .arrayReturnType(true) + .returnTypeMandatory(false) + .arrayLevels(1) + .arrayReturnTypeMandatory(true) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("superPowers: [String!]")); + } + + @Test + public void testFieldDefinitionWith1MandatoryArgument() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .addArgument(createArgument("filter", "String", true, null, STRING)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\nfilter: String!\n): Person!")); + } + + @Test + public void testFieldDefinitionWith1MandatoryArgumentAndArrayType() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(1) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\nfilter: String\n): [Person]!")); + } + + @Test + public void testFieldDefinitionWithMultipleArguments() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .addArgument(createArgument("age", "Int", true, null, INTEGER)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), is("person(\nfilter: String, \nage: Int!\n): Person!")); + } + + @Test + public void testFieldDefinitionWithMultipleArgumentsAndDescription() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + SchemaArgument schemaArgument1 = createArgument("filter", "String", false, null, STRING); + schemaArgument1.description("Optional filter"); + SchemaArgument schemaArgument2 = createArgument("age", "Int", true, null, INTEGER); + schemaArgument2.description("Mandatory age"); + schemaFieldDefinition.addArgument(schemaArgument1); + schemaFieldDefinition.addArgument(schemaArgument2); + assertThat(schemaFieldDefinition.getSchemaAsString(), + is("person(\n\"Optional filter\"\nfilter: String, \n\"Mandatory age\"\nage: Int!\n): Person!")); + } + + @Test + public void testFieldDefinitionWithMultipleArgumentsAndBothDescriptions() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .description("Description of field definition") + .build(); + + SchemaArgument schemaArgument1 = createArgument("filter", "String", false, null, STRING); + schemaArgument1.description("Optional filter"); + SchemaArgument schemaArgument2 = createArgument("age", "Int", true, null, INTEGER); + schemaArgument2.description("Mandatory age"); + schemaFieldDefinition.addArgument(schemaArgument1); + schemaFieldDefinition.addArgument(schemaArgument2); + assertThat(schemaFieldDefinition.getSchemaAsString(), + is("\"Description of field definition\"\nperson(\n\"Optional filter\"\nfilter: String, \n\"Mandatory " + + "age\"\nage: " + + "Int!\n): Person!")); + } + + @Test + public void testFieldDefinitionWithMultipleArgumentsWithArray() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(false) + .arrayLevels(1) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .addArgument(createArgument("age", "Int", true, null, INTEGER)) + .addArgument(createArgument("job", "String", false, null, STRING)) + .build(); + + assertThat(schemaFieldDefinition.getSchemaAsString(), + is("person(\nfilter: String, \nage: Int!, \njob: String\n): [Person]")); + } + + @Test + public void testDataFetchers() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(false) + .arrayLevels(0) + .build(); + + assertThat(schemaFieldDefinition.dataFetcher(), is(nullValue())); + schemaFieldDefinition.dataFetcher(new StaticDataFetcher("Value")); + DataFetcher dataFetcher = schemaFieldDefinition.dataFetcher(); + assertThat(dataFetcher, is(notNullValue())); + assertThat(dataFetcher instanceof StaticDataFetcher, is(true)); + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaGeneratorTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaGeneratorTest.java new file mode 100644 index 00000000000..758d3b9f533 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaGeneratorTest.java @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2020 and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.json.bind.annotation.JsonbNumberFormat; + +import io.helidon.microprofile.graphql.server.test.enums.EnumTestNoEnumName; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithNameAndNameAnnotation; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithNameAnnotation; +import io.helidon.microprofile.graphql.server.test.queries.OddNamedQueriesAndMutations; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesAndMutations; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesWithSource; +import io.helidon.microprofile.graphql.server.test.types.Address; +import io.helidon.microprofile.graphql.server.test.types.Car; +import io.helidon.microprofile.graphql.server.test.types.DefaultValuePOJO; +import io.helidon.microprofile.graphql.server.test.types.InnerClass; +import io.helidon.microprofile.graphql.server.test.types.InterfaceWithTypeValue; +import io.helidon.microprofile.graphql.server.test.types.Level0; +import io.helidon.microprofile.graphql.server.test.types.Level1; +import io.helidon.microprofile.graphql.server.test.types.Motorbike; +import io.helidon.microprofile.graphql.server.test.types.MultiLevelListsAndArrays; +import io.helidon.microprofile.graphql.server.test.types.ObjectWithIgnorableFieldsAndMethods; +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.graphql.server.test.types.PersonWithName; +import io.helidon.microprofile.graphql.server.test.types.PersonWithNameValue; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithNumberFormats; +import io.helidon.microprofile.graphql.server.test.types.TypeWithIDs; +import io.helidon.microprofile.graphql.server.test.types.TypeWithIdOnField; +import io.helidon.microprofile.graphql.server.test.types.TypeWithIdOnMethod; +import io.helidon.microprofile.graphql.server.test.types.TypeWithNameAndJsonbProperty; +import io.helidon.microprofile.graphql.server.test.types.Vehicle; +import io.helidon.microprofile.graphql.server.test.types.VehicleIncident; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NonNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.microprofile.graphql.server.FormattingHelper.DATE; +import static io.helidon.microprofile.graphql.server.FormattingHelper.NUMBER; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getFieldFormat; +import static io.helidon.microprofile.graphql.server.FormattingHelper.getNumberFormatAnnotation; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DEFAULT_LOCALE; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getAnnotationValue; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDefaultDescription; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getParameterAnnotations; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.stripMethodName; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link SchemaGenerator}. + */ +class SchemaGeneratorTest extends AbstractGraphQLTest { + + private static final String ADDRESS = Address.class.getName(); + private static final String STRING = String.class.getName(); + private static final String BIGDECIMAL = BigDecimal.class.getName(); + private static final String COLLECTION = Collection.class.getName(); + private static final String LIST = List.class.getName(); + private static final String LOCALDATE = LocalDate.class.getName(); + private static final String ID = "ID"; + + private List listStringArray = new ArrayList<>(); + private List listString = new ArrayList<>(); + private List<@org.eclipse.microprofile.graphql.NumberFormat("0 'number'") Integer> listIntegerFormatted = new ArrayList<>(); + private List<@DateFormat("dd/mm/yyyy") LocalDate> listDateFormatted = new ArrayList<>(); + private List<@org.eclipse.microprofile.graphql.NumberFormat(value = "0 'number'", locale = "en-AU") Integer> + listIntegerFormattedWithLocale = new ArrayList<>(); + private List>> listListString = new ArrayList<>(); + + @org.eclipse.microprofile.graphql.NumberFormat("0 'number'") + private List listIntegerFormat2; + + public List getListStringArray() { + return listStringArray; + } + + public List getListString() { + return listString; + } + + public List>> getListListString() { + return listListString; + } + + @org.eclipse.microprofile.graphql.NumberFormat("0 'method'") + public List getListIntegerWithFormat( + @Name("name") List<@org.eclipse.microprofile.graphql.NumberFormat("0 'number'") @NonNull Integer> value) { + return value; + } + + public List> getListListIntegerWithFormat( + @Name("name") List> value) { + return value; + } + + @JsonbNumberFormat("0 'jsonb'") + @org.eclipse.microprofile.graphql.NumberFormat("0 'number'") + public List> getListListIntegerWith2Formats( + @Name("name") List> value) { + return value; + } + + public List> getListListLocalDateWithFormat( + @Name("name") List> value) { + return value; + } + + public List> getListListLocalDateWithFormatAndLocale( + @Name("name") List> value) { + return value; + } + + public Collection> getCollectionListIntegerWithFormat( + @Name("name") Collection> value) { + return value; + } + + @org.eclipse.microprofile.graphql.NumberFormat("0 'number'") + public List getListIntegerFormatted2(List value) { + return value; + } + + private SchemaGenerator schemaGenerator; + + @BeforeEach + public void beforeEach() { + schemaGenerator = createSchemaGenerator(); + } + + @Test + public void testFieldFormats() throws NoSuchFieldException { + Field field = SchemaGeneratorTest.class.getDeclaredField("listString"); + assertThat(field, is(notNullValue())); + Annotation[] annotations = getFieldAnnotations(field, 0); + assertThat(annotations, is(notNullValue())); + + assertFieldFormat(SchemaGeneratorTest.class.getDeclaredField("listString"), + new String[] {null, null, null}); + assertFieldFormat(SchemaGeneratorTest.class.getDeclaredField("listIntegerFormatted"), + new String[] {NUMBER, "0 'number'", DEFAULT_LOCALE}); + assertFieldFormat(SchemaGeneratorTest.class.getDeclaredField("listIntegerFormattedWithLocale"), + new String[] {NUMBER, "0 'number'", "en-AU"}); + assertFieldFormat(SchemaGeneratorTest.class.getDeclaredField("listDateFormatted"), + new String[] {DATE, "dd/mm/yyyy", DEFAULT_LOCALE}); + assertFieldFormat(SchemaGeneratorTest.class.getDeclaredField("listIntegerFormat2"), + new String[] {NUMBER, "0 'number'", DEFAULT_LOCALE}); + } + + @Test + public void testMethodFormats() throws NoSuchMethodException { + assertMethodFormat(SchemaGeneratorTest.class.getMethod("getListIntegerWithFormat", List.class), + new String[] {NUMBER, "0 'method'", DEFAULT_LOCALE}); + assertMethodFormat(SchemaGeneratorTest.class.getMethod("getListListIntegerWithFormat", List.class), + new String[] {NUMBER, "0 'number'", DEFAULT_LOCALE}); + assertMethodFormat(SchemaGeneratorTest.class.getMethod("getListListIntegerWith2Formats", List.class), + new String[] {NUMBER, "0 'number'", DEFAULT_LOCALE}); + } + + @Test + public void testParameterFormatsAndNulls() throws NoSuchMethodException { + Method method = SchemaGeneratorTest.class.getMethod("getListIntegerWithFormat", List.class); + assertThat(method, is(notNullValue())); + Parameter parameter = method.getParameters()[0]; + + Annotation[] parameterAnnotations = getParameterAnnotations(parameter, 0); + assertThat(parameterAnnotations, is(notNullValue())); + assertThat(parameterAnnotations.length, is(2)); + assertThat(getAnnotationValue(parameterAnnotations, NonNull.class), is(notNullValue())); + + assertParameterFormat(SchemaGeneratorTest.class.getMethod("getListIntegerWithFormat", List.class), 0, + new String[] { NUMBER, "0 'number'", DEFAULT_LOCALE }); + assertParameterFormat(SchemaGeneratorTest.class.getMethod("getListListIntegerWithFormat", List.class), 0, + new String[] { NUMBER, "0 'number'", DEFAULT_LOCALE }); + assertParameterFormat(SchemaGeneratorTest.class.getMethod("getCollectionListIntegerWithFormat", Collection.class), 0, + new String[] { NUMBER, "0 'number'", DEFAULT_LOCALE }); + assertParameterFormat(SchemaGeneratorTest.class.getMethod("getListListLocalDateWithFormat", List.class), 0, + new String[] { DATE, "dd/mm/yyyy", DEFAULT_LOCALE }); + assertParameterFormat(SchemaGeneratorTest.class.getMethod("getListListLocalDateWithFormatAndLocale", List.class), 0, + new String[] { DATE, "dd/mm/yyyy", "en-AU" }); + + } + + @Test + public void testEnumGeneration() throws IntrospectionException, ClassNotFoundException { + testEnum(EnumTestNoEnumName.class, EnumTestNoEnumName.class.getSimpleName()); + testEnum(EnumTestWithEnumName.class, "TShirtSize"); + testEnum(EnumTestWithNameAnnotation.class, "TShirtSize"); + testEnum(EnumTestWithNameAndNameAnnotation.class, "ThisShouldWin"); + } + + @Test + public void testGettersPerson() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator.retrieveGetterBeanMethods(Person.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(13)); + + assertDiscoveredMethod(mapMethods.get("personId"), "personId", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("name"), "name", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("homeAddress"), "homeAddress", ADDRESS, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("workAddress"), "workAddress", ADDRESS, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("creditLimit"), "creditLimit", BIGDECIMAL, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("listQualifications"), "listQualifications", STRING, COLLECTION, true, true, + false); + assertDiscoveredMethod(mapMethods.get("previousAddresses"), "previousAddresses", ADDRESS, LIST, true, true, false); + assertDiscoveredMethod(mapMethods.get("intArray"), "intArray", "int", null, true, false, false); + assertDiscoveredMethod(mapMethods.get("stringArray"), "stringArray", STRING, null, true, false, false); + assertDiscoveredMethod(mapMethods.get("addressMap"), "addressMap", ADDRESS, null, true, false, true); + assertDiscoveredMethod(mapMethods.get("localDate"), "localDate", LOCALDATE, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("longValue"), "longValue", long.class.getName(), null, false, false, false); + assertDiscoveredMethod(mapMethods.get("bigDecimal"), "bigDecimal", BigDecimal.class.getName(), null, false, false, false); + } + + @Test + public void testMultipleLevels() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator.retrieveGetterBeanMethods(Level0.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(2)); + assertDiscoveredMethod(mapMethods.get("id"), "id", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("level1"), "level1", Level1.class.getName(), null, false, false, false); + } + + @Test + public void testTypeWithIdOnMethod() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(TypeWithIdOnMethod.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(2)); + assertDiscoveredMethod(mapMethods.get("name"), "name", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("id"), "id", ID, null, false, false, false); + } + + @Test + public void testTypeWithIdOnField() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(TypeWithIdOnField.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(2)); + assertDiscoveredMethod(mapMethods.get("name"), "name", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("id"), "id", ID, null, false, false, false); + } + + @Test + public void testTypeWithNameAndJsonbProperty() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(TypeWithNameAndJsonbProperty.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(6)); + assertDiscoveredMethod(mapMethods.get("newFieldName1"), "newFieldName1", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("newFieldName2"), "newFieldName2", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("newFieldName3"), "newFieldName3", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("newFieldName4"), "newFieldName4", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("newFieldName5"), "newFieldName5", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("newFieldName6"), "newFieldName6", STRING, null, false, false, false); + } + + @Test + public void testInterfaceDiscovery() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(Vehicle.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(6)); + assertDiscoveredMethod(mapMethods.get("plate"), "plate", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("make"), "make", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("model"), "model", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("numberOfWheels"), "numberOfWheels", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("manufactureYear"), "manufactureYear", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("incidents"), "incidents", VehicleIncident.class.getName(), COLLECTION, true, true, + false); + } + + @Test + public void testInterfaceImplementorDiscovery1() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator.retrieveGetterBeanMethods(Car.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(7)); + assertDiscoveredMethod(mapMethods.get("plate"), "plate", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("make"), "make", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("model"), "model", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("numberOfWheels"), "numberOfWheels", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("manufactureYear"), "manufactureYear", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("numberOfDoors"), "numberOfDoors", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("incidents"), "incidents", VehicleIncident.class.getName(), COLLECTION, true, true, + false); + } + + @Test + public void testInterfaceImplementorDiscovery2() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(Motorbike.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(7)); + assertDiscoveredMethod(mapMethods.get("plate"), "plate", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("make"), "make", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("model"), "model", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("numberOfWheels"), "numberOfWheels", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("manufactureYear"), "manufactureYear", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("hasSideCar"), "hasSideCar", "boolean", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("incidents"), "incidents", VehicleIncident.class.getName(), COLLECTION, true, true, + false); + } + + @Test + public void testObjectWithIgnorableFields() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator.retrieveGetterBeanMethods( + ObjectWithIgnorableFieldsAndMethods.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(3)); + assertDiscoveredMethod(mapMethods.get("id"), "id", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("dontIgnore"), "dontIgnore", boolean.class.getName(), null, false, false, false); + assertDiscoveredMethod(mapMethods.get("value"), "value", STRING, null, false, false, false); + + } + + @Test + public void testValidNames() { + assertThat(SchemaGeneratorHelper.isValidGraphQLName("Name"), is(true)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("1Name"), is(false)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("Name#"), is(false)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("InvalidName#"), is(false)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("valid_name"), is(true)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("_valid_name"), is(true)); + assertThat(SchemaGeneratorHelper.isValidGraphQLName("__valid_name"), is(false)); + } + + @Test + public void testTypeNames() { + assertThat(SchemaGeneratorHelper.getTypeName(Address.class), is("Address")); + assertThat(SchemaGeneratorHelper.getTypeName(PersonWithName.class), is("Person")); + assertThat(SchemaGeneratorHelper.getTypeName(Person.class), is("Person")); + assertThat(SchemaGeneratorHelper.getTypeName(VehicleIncident.class), is("Incident")); + assertThat(SchemaGeneratorHelper.getTypeName(PersonWithNameValue.class), is("Person")); + assertThat(SchemaGeneratorHelper.getTypeName(Vehicle.class), is("Vehicle")); + assertThat(SchemaGeneratorHelper.getTypeName(EnumTestNoEnumName.class), is("EnumTestNoEnumName")); + assertThat(SchemaGeneratorHelper.getTypeName(EnumTestWithEnumName.class), is("TShirtSize")); + assertThat(SchemaGeneratorHelper.getTypeName(EnumTestWithNameAndNameAnnotation.class), is("ThisShouldWin")); + assertThat(SchemaGeneratorHelper.getTypeName(EnumTestWithNameAnnotation.class), is("TShirtSize")); + assertThat(SchemaGeneratorHelper.getTypeName(InnerClass.AnInnerClass.class), is("AnInnerClass")); + assertThat(SchemaGeneratorHelper.getTypeName(InnerClass.class), is("InnerClass")); + assertThat(SchemaGeneratorHelper.getTypeName(InterfaceWithTypeValue.class), is("NewName")); + } + + @Test + public void testGetSimpleName() throws ClassNotFoundException { + assertThat(SchemaGeneratorHelper.getSimpleName(InnerClass.AnInnerClass.class.getName()), is("AnInnerClass")); + assertThat(SchemaGeneratorHelper.getSimpleName(int.class.getName()), is(int.class.getName())); + assertThat(SchemaGeneratorHelper.getSimpleName(SchemaGeneratorHelper.ID), is(SchemaGeneratorHelper.ID)); + assertThat(SchemaGeneratorHelper.getSimpleName(SchemaGeneratorHelper.BOOLEAN), is(SchemaGeneratorHelper.BOOLEAN)); + assertThat(SchemaGeneratorHelper.getSimpleName(SchemaGeneratorHelper.STRING), is(SchemaGeneratorHelper.STRING)); + } + + @Test + public void testAllMethods() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveAllAnnotatedBeanMethods(SimpleQueriesAndMutations.class); + assertThat(mapMethods, is(notNullValue())); + + assertDiscoveredMethod(mapMethods.get("hero"), "hero", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("episodeCount"), "episodeCount", "int", null, false, false, false); + assertDiscoveredMethod(mapMethods.get("numberOfStars"), "numberOfStars", Long.class.getName(), null, false, false, false); + assertDiscoveredMethod(mapMethods.get("badGuy"), "badGuy", STRING, null, false, false, false); + assertDiscoveredMethod(mapMethods.get("allPeople"), "allPeople", Person.class.getName(), COLLECTION, true, true, false); + assertDiscoveredMethod(mapMethods.get("returnCurrentDate"), "returnCurrentDate", LocalDate.class.getName(), null, false, + false, false); + assertDiscoveredMethod(mapMethods.get("returnMediumSize"), "returnMediumSize", EnumTestWithEnumName.class.getName(), null, + false, false, false); + assertDiscoveredMethod(mapMethods.get("returnTypeWithIDs"), "returnTypeWithIDs", TypeWithIDs.class.getName(), null, + false, false, false); + assertDiscoveredMethod(mapMethods.get("getMultiLevelList"), "getMultiLevelList", MultiLevelListsAndArrays.class.getName(), + null, + false, false, false); + assertDiscoveredMethod(mapMethods.get("idQuery"), "idQuery", ID, null, + false, false, false); + assertDiscoveredMethod(mapMethods.get("booleanObject"), "booleanObject", Boolean.class.getName(), null, + false, false, false); + assertDiscoveredMethod(mapMethods.get("booleanPrimitive"), "booleanPrimitive", boolean.class.getName(), null, + false, false, false); + } + + @Test + public void testStripMethodName() throws NoSuchMethodException { + Method method = SimpleQueriesWithSource.class.getMethod("getCurrentJob", SimpleContact.class); + assertThat(stripMethodName(method, false), is("currentJob")); + method = DefaultValuePOJO.class.getMethod("getId"); + assertThat(stripMethodName(method, true), is("id")); + method = OddNamedQueriesAndMutations.class.getMethod("settlement"); + assertThat(stripMethodName(method, false), is("settlement")); + method = OddNamedQueriesAndMutations.class.getMethod("getaway"); + assertThat(stripMethodName(method, false), is("getaway")); + } + + @Test + public void testDefaultDescription() { + assertThat(getDefaultDescription(null, null), is(nullValue())); + assertThat(getDefaultDescription(new String[] { "format", "locale" }, null), is("format locale")); + assertThat(getDefaultDescription(null, "desc"), is("desc")); + assertThat(getDefaultDescription(new String[] { "format", "locale" }, "desc"), is("desc (format locale)")); + } + + @Test + public void testArrayDiscoveredMethods() throws IntrospectionException, ClassNotFoundException { + Map mapMethods = schemaGenerator + .retrieveGetterBeanMethods(MultiLevelListsAndArrays.class, false); + assertThat(mapMethods, is(notNullValue())); + assertThat(mapMethods.size(), is(8)); + assertDiscoveredMethod(mapMethods.get("multiStringArray"), "multiStringArray", STRING, null, true, false, false); + } + + private void assertDiscoveredMethod(SchemaGeneratorHelper.DiscoveredMethod discoveredMethod, + String name, + String returnType, + String collectionType, + boolean isArrayReturnType, + boolean isCollectionType, + boolean isMap) { + assertThat(discoveredMethod, is(notNullValue())); + assertThat(discoveredMethod.isCollectionType(), is(isCollectionType)); + assertThat(discoveredMethod.isArrayReturnType(), is(isArrayReturnType)); + assertThat(discoveredMethod.isMap(), is(isMap)); + assertThat(discoveredMethod.name(), is(name)); + assertThat(discoveredMethod.returnType(), is(returnType)); + assertThat(discoveredMethod.collectionType(), is(collectionType)); + } + + @Test + public void testArrayLevelsAndRootArray() { + String[] oneLevelString = new String[1]; + String[][] twoLevelString = new String[2][1]; + String[][][] threeLevelString = new String[2][1][2]; + int[] oneLevelInt = new int[1]; + int[][] twoLevelInt = new int[1][1]; + int[][][] threeLevelInt = new int[1][1][2]; + + assertThat(SchemaGeneratorHelper.getArrayLevels(oneLevelString.getClass().getName()), is(1)); + assertThat(SchemaGeneratorHelper.getArrayLevels(twoLevelString.getClass().getName()), is(2)); + assertThat(SchemaGeneratorHelper.getArrayLevels(threeLevelString.getClass().getName()), is(3)); + assertThat(SchemaGeneratorHelper.getArrayLevels(oneLevelInt.getClass().getName()), is(1)); + assertThat(SchemaGeneratorHelper.getArrayLevels(twoLevelInt.getClass().getName()), is(2)); + assertThat(SchemaGeneratorHelper.getArrayLevels(threeLevelInt.getClass().getName()), is(3)); + + assertThat(SchemaGeneratorHelper.getRootArrayClass(oneLevelString.getClass().getName()), is(String.class.getName())); + assertThat(SchemaGeneratorHelper.getRootArrayClass(twoLevelString.getClass().getName()), is(String.class.getName())); + assertThat(SchemaGeneratorHelper.getRootArrayClass(threeLevelString.getClass().getName()), is(String.class.getName())); + assertThat(SchemaGeneratorHelper.getRootArrayClass(oneLevelInt.getClass().getName()), is(int.class.getName())); + assertThat(SchemaGeneratorHelper.getRootArrayClass(twoLevelInt.getClass().getName()), is(int.class.getName())); + assertThat(SchemaGeneratorHelper.getRootArrayClass(threeLevelInt.getClass().getName()), is(int.class.getName())); + } + + @Test + public void testGetRootType() throws NoSuchFieldException, NoSuchMethodException { + SchemaGenerator schemaGenerator = createSchemaGenerator(); + + ParameterizedType stringArrayListType = getParameterizedType("listStringArray"); + SchemaGenerator.RootTypeResult rootTypeName = + schemaGenerator.getRootTypeName(stringArrayListType.getActualTypeArguments()[0], 0, + -1, + SchemaGeneratorTest.class.getMethod("getListStringArray")); + assertThat(rootTypeName.rootTypeName(), is(String[].class.getName())); + assertThat(rootTypeName.levels(), is(1)); + + ParameterizedType stringListType = getParameterizedType("listString"); + rootTypeName = schemaGenerator.getRootTypeName(stringListType.getActualTypeArguments()[0], 0, + -1, + SchemaGeneratorTest.class.getMethod("getListStringArray")); + assertThat(rootTypeName.rootTypeName(), is(STRING)); + assertThat(rootTypeName.levels(), is(1)); + + ParameterizedType listListStringType = getParameterizedType("listListString"); + rootTypeName = schemaGenerator.getRootTypeName(listListStringType.getActualTypeArguments()[0], 0, + -1, + SchemaGeneratorTest.class.getMethod("getListListString")); + assertThat(rootTypeName.rootTypeName(), is(STRING)); + assertThat(rootTypeName.levels(), is(2)); + } + + @Test + public void testGetCorrectFormat() { + NumberFormat numberFormat = FormattingHelper.getCorrectNumberFormat(INT, ""); + assertThat(numberFormat, is(notNullValue())); + assertThat(numberFormat.getMaximumFractionDigits(), is(0)); + + numberFormat = FormattingHelper.getCorrectNumberFormat(BIG_INTEGER, ""); + assertThat(numberFormat, is(notNullValue())); + assertThat(numberFormat.getMaximumFractionDigits(), is(0)); + + numberFormat = FormattingHelper.getCorrectNumberFormat(FLOAT, ""); + assertThat(numberFormat, is(notNullValue())); + assertThat(numberFormat.getMaximumFractionDigits() > 0, is(true)); + + numberFormat = FormattingHelper.getCorrectNumberFormat(BIG_DECIMAL, ""); + assertThat(numberFormat, is(notNullValue())); + assertThat(numberFormat.getMaximumFractionDigits() > 0, is(true)); + } + + @Test + public void testFormatting() { + assertFormat(FLOAT, "en-ZA", "¤ 000.00", 100.0d, "R 100,00"); + assertFormat(FLOAT, "en-AU", "¤ 000.00", 100.0d, "$ 100.00"); + assertFormat(FLOAT, "en-AU", "000.00 'ml'", 125.12d, "125.12 ml"); + assertFormat(INT, "en-AU", "0 'years'", 52, "52 years"); + assertFormat(INT, "en-AU", "0 'years old'", 12, "12 years old"); + assertFormat(BIG_DECIMAL, "en-AU", "###,###.###", 123456.789, "123,456.789"); + assertFormat(BIG_DECIMAL, "en-AU", "000000.000", 123.78, "000123.780"); + } + + @Test + public void testGetFormatAnnotation() throws NoSuchFieldException, NoSuchMethodException { + assertAnnotation(SimpleContactWithNumberFormats.class, "field", "age", 2, "0 'years old'", DEFAULT_LOCALE); + assertAnnotation(SimpleContactWithNumberFormats.class, "field", "bankBalance", 2, "¤ 000.00", "en-AU"); + assertAnnotation(SimpleContactWithNumberFormats.class, "field", "value", 2, "0 'value'", DEFAULT_LOCALE); + assertAnnotation(SimpleContactWithNumberFormats.class, "field", "name", 0, null, null); + + assertAnnotation(SimpleContactWithNumberFormats.class, "methodParam", "getFormatMethod", 2, "0 'years old'", + DEFAULT_LOCALE, + int.class); + assertAnnotation(SimpleContactWithNumberFormats.class, "methodParam", "getName", 0, null, null); + + assertAnnotation(SimpleContactWithNumberFormats.class, "method", "getId", 2, "0 'id'", DEFAULT_LOCALE); + assertAnnotation(SimpleContactWithNumberFormats.class, "method", "getName", 0, null, null); + } + + @Test + public void testFormatAnnotationFromSchema() throws IntrospectionException, ClassNotFoundException { + SchemaGenerator schemaGenerator = createSchemaGenerator(); + Schema schema = schemaGenerator.generateSchemaFromClasses(Set.of(SimpleContactWithNumberFormats.class)); + assertThat(schema, is(notNullValue())); + } + + @Test + public void testPrimitiveArrays() { + ArrayList a; + } + + private void assertParameterFormat(Method method, int paramNumber, String[] expectedFormat) { + assertThat(method, is(notNullValue())); + Parameter parameter = method.getParameters()[paramNumber]; + String[] format = FormattingHelper.getMethodParameterFormat(parameter, 0); + assertThat(format, is(notNullValue())); + assertThat(format.length, is(3)); + assertThat(format, is(expectedFormat)); + } + + private void assertFieldFormat(Field field, String[] expectedFormat) { + assertThat(field, is(notNullValue())); + String[] format = getFieldFormat(field, 0); + assertThat(format, is(notNullValue())); + assertThat(format.length, is(3)); + assertThat(format, is(expectedFormat)); + } + + private void assertMethodFormat(Method method, String[] expectedFormat) { + assertThat(method, is(notNullValue())); + String[] format = FormattingHelper.getMethodFormat(method, 0); + assertThat(format, is(notNullValue())); + assertThat(format.length, is(3)); + assertThat(format, is(expectedFormat)); + } + + /** + * Assert that a {@link org.eclipse.microprofile.graphql.NumberFormat} or {@link javax.json.bind.annotation.JsonbNumberFormat} + * annotation is correctly applied. + * + * @param clazz {@link Class} to apply to + * @param type type to check, "field", "method" or "methodParam" + * @param name field name or method name + * @param expectedLength expected length of format array + * @param expectedFormat expected value for format + * @param expectedLocale expected value for locale + * @param methodArgs arguments of the method + * @throws NoSuchFieldException + */ + private void assertAnnotation(Class clazz, + String type, + String name, + int expectedLength, + String expectedFormat, + String expectedLocale, + Class... methodArgs) + throws NoSuchFieldException, NoSuchMethodException { + if (expectedLength != 0 && expectedLength != 2) { + throw new IllegalArgumentException("Expected length should be 0 or 2"); + } + String[] annotation = new String[0]; + if ("field".equals(type)) { + Field field = clazz.getDeclaredField(name); + assertThat(field, is(notNullValue())); + annotation = getNumberFormatAnnotation(field); + } else if ("methodParam".equals(type)) { + Method method = clazz.getMethod(name, methodArgs); + assertThat(method, is(notNullValue())); + if (expectedLength == 2) { + annotation = getNumberFormatAnnotation(method.getParameters()[0]); + } + } else if ("method".equals(type)) { + Method method = clazz.getMethod(name, methodArgs); + assertThat(method, is(notNullValue())); + if (expectedLength == 2) { + annotation = getNumberFormatAnnotation(method); + } + } else { + throw new IllegalArgumentException("Unknown type of " + type); + } + assertThat(annotation, is(notNullValue())); + assertThat(annotation.length, is(expectedLength)); + if (expectedLength == 2) { + assertThat("Format should be " + expectedFormat + " but is " + annotation[0], annotation[0], is(expectedFormat)); + assertThat("locale should be " + expectedLocale + " but is " + annotation[1], annotation[1], is(expectedLocale)); + } + } + + /** + * Assert formatting is correct. + * + * @param type type to format + * @param locale locale to use + * @param format format to apply + * @param value value to format + * @param expectedResult expected result + */ + private void assertFormat(String type, String locale, String format, Object value, String expectedResult) { + NumberFormat numberFormat = FormattingHelper.getCorrectNumberFormat(type, locale, format); + assertThat(numberFormat, is(notNullValue())); + String formatted = numberFormat.format(value); + assertThat(formatted, is(notNullValue())); + assertThat(formatted, is(expectedResult)); + } + + private ParameterizedType getParameterizedType(String fieldName) throws NoSuchFieldException { + Field listStringArray = SchemaGeneratorTest.class.getDeclaredField(fieldName); + return (ParameterizedType) listStringArray.getGenericType(); + } + + private void testEnum(Class clazz, String expectedName) throws IntrospectionException, ClassNotFoundException { + SchemaGenerator schemaGenerator = createSchemaGenerator(); + Schema schema = schemaGenerator.generateSchemaFromClasses(Set.of(clazz)); + assertThat(schema, is(notNullValue())); + + assertThat(schema.getEnums().size(), is(1)); + SchemaEnum schemaEnumResult = schema.getEnumByName(expectedName); + + assertThat(schemaEnumResult, is(notNullValue())); + assertThat(schemaEnumResult.values().size(), is(6)); + + Arrays.stream(new String[] { "S", "M", "L", "XL", "XXL", "XXXL" }) + .forEach(v -> assertThat(schemaEnumResult.values().contains(v), is(true))); + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaScalarTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaScalarTest.java new file mode 100644 index 00000000000..34c6379db84 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaScalarTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.Date; + +import graphql.scalars.ExtendedScalars; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +/** + * Tests for {@link SchemaScalar} class. + */ +class SchemaScalarTest { + + @Test + public void testConstructors() { + SchemaScalar schemaScalar = new SchemaScalar("myName", Integer.class.getName(), ExtendedScalars.DateTime, null); + assertThat(schemaScalar.name(), is("myName")); + assertThat(schemaScalar.actualClass(), is(Integer.class.getName())); + assertThat(schemaScalar.graphQLScalarType().equals(ExtendedScalars.DateTime), is(true)); + assertThat(schemaScalar.defaultFormat(), is(nullValue())); + schemaScalar.defaultFormat("ABC"); + assertThat(schemaScalar.defaultFormat(), is("ABC")); + } + + @Test + public void testGetScalarAsString() { + assertThat(new SchemaScalar("Test", Date.class.getName(), ExtendedScalars.DateTime, null).getSchemaAsString(), is("scalar Test")); + assertThat(new SchemaScalar("Date", Date.class.getName(), ExtendedScalars.DateTime, null).getSchemaAsString(), is("scalar Date")); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTest.java new file mode 100644 index 00000000000..dc7ab9726d7 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import graphql.scalars.ExtendedScalars; +import org.junit.jupiter.api.Test; + +import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION; +import static io.helidon.microprofile.graphql.server.TestHelper.createArgument; +import static io.helidon.microprofile.graphql.server.TestHelper.createSchemaType; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link Schema} class. + */ +class SchemaTest extends AbstractGraphQLTest { + + @Test + public void testScalars() { + Schema schema = Schema.create(); + assertThat(schema.getScalars().size(), is(0)); + + SchemaScalar schemaScalar1 = new SchemaScalar("Test", Date.class.getName(), ExtendedScalars.Date, null); + SchemaScalar schemaScalar2 = new SchemaScalar("Test2", Date.class.getName(), ExtendedScalars.Date, "XYZ"); + schema.addScalar(schemaScalar1); + schema.addScalar(schemaScalar2); + + assertThat(schema.getScalars().contains(schemaScalar1), is(true)); + assertThat(schema.getScalars().contains(schemaScalar2), is(true)); + + assertThat(schema.getScalarByActualClass(Date.class.getName()), is(notNullValue())); + } + + @Test + public void testDirectives() { + Schema schema = Schema.create(); + assertThat(schema.getDirectives().size(), is(0)); + + SchemaDirective schemaDirective1 = SchemaDirective.builder().name("directive1").build(); + SchemaDirective schemaDirective2 = SchemaDirective.builder().name("directive2").build(); + schema.addDirective(schemaDirective1); + schema.addDirective(schemaDirective2); + assertThat(schema.getDirectives().contains(schemaDirective1), is(true)); + assertThat(schema.getDirectives().contains(schemaDirective2), is(true)); + } + + @Test + public void tesTypes() { + Schema schema = Schema.create(); + assertThat(schema.getInputTypes().size(), is(0)); + SchemaType schemaType1 = createSchemaType("name", "valueClass"); + SchemaType schemaType2 = createSchemaType("name2", "valueClass2"); + schema.addType(schemaType1); + schema.addType(schemaType2); + assertThat(schema.getTypes().size(), is(2)); + assertThat(schema.getTypes().contains(schemaType1), is(true)); + assertThat(schema.getTypes().contains(schemaType2), is(true)); + + assertThat(schema.getTypeByName("nothing"), is(nullValue())); + } + + @Test + public void testEnums() { + Schema schema = Schema.create(); + assertThat(schema.getEnums().size(), is(0)); + List listString = new ArrayList<>(); + listString.add("NEWHOPE"); + listString.add("JEDI"); + listString.add("EMPIRE"); + SchemaEnum schemaEnum1 = SchemaEnum.builder().name("Episode").build(); + schemaEnum1.values().addAll(listString); + + schema.addEnum(schemaEnum1); + assertThat(schema.getEnums().size(), is(1)); + assertThat(schema.getEnums().contains(schemaEnum1), is(true)); + + assertThat(schema.getEnumByName("nothing"), is(nullValue())); + assertThat(schema.containsEnumWithName("nothing"), is(false)); + assertThat(schema.containsEnumWithName("Episode"), is(true)); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTypeTest.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTypeTest.java new file mode 100644 index 00000000000..2e593a03b99 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/SchemaTypeTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import org.junit.jupiter.api.Test; + +import static io.helidon.microprofile.graphql.server.TestHelper.createArgument; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests for {@link SchemaType} and {@link SchemaInputType} classes. + */ +class SchemaTypeTest { + + private static final Class STRING = String.class; + private static final Class INTEGER = Integer.class; + + @Test + public void testBuilders() { + SchemaType schemaType = SchemaType.builder() + .name("Name") + .valueClassName("com.oracle.test.Value") + .build(); + + assertThat(schemaType.name(), is("Name")); + assertThat(schemaType.valueClassName(), is("com.oracle.test.Value")); + assertThat(schemaType.fieldDefinitions(), is(notNullValue())); + + schemaType.addFieldDefinition(SchemaFieldDefinition.builder() + .name("orderId") + .returnType("Integer") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build()); + schemaType.addFieldDefinition(SchemaFieldDefinition.builder() + .name("personId") + .returnType("Integer") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build()); + schemaType.addFieldDefinition(SchemaFieldDefinition.builder() + .name("personId") + .returnType("Integer") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build()); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("orders") + .returnType("Order") + .arrayReturnType(true) + .returnTypeMandatory(true) + .arrayLevels(0) + .addArgument(createArgument("filter", "String", false, null, STRING)) + .build(); + + schemaType.addFieldDefinition(schemaFieldDefinition); + + assertThat(schemaType.fieldDefinitions().size(), is(4)); + assertThat(schemaType.fieldDefinitions().contains(schemaFieldDefinition), is(true)); + + assertThat(schemaType.implementingInterface(), is(nullValue())); + schemaType.implementingInterface("Contact"); + assertThat(schemaType.implementingInterface(), is("Contact")); + + schemaType.name("Name"); + assertThat(schemaType.name(), is("Name")); + } + + @Test + public void testImplementingInterface() { + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaFieldDefinition.addArgument(createArgument("age", "Int", true, null, INTEGER)); + + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .addFieldDefinition(schemaFieldDefinition) + .implementingInterface("Contact") + .build(); + + assertThat(schemaType.getSchemaAsString(), is("type Person implements Contact {\n" + + "person(\nfilter: String, \nage: Int!\n): Person!\n" + + "}\n")); + } + + @Test + public void testTypeSchemaOutput() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaType.addFieldDefinition(schemaFieldDefinition); + + assertThat(schemaType.fieldDefinitions().get(0).equals(schemaFieldDefinition), is(true)); + + assertThat(schemaType.getSchemaAsString(), is("type Person {\n" + + "person(\nfilter: String\n): Person!\n" + + "}\n")); + assertThat(schemaType.getGraphQLName(), is("type")); + } + + @Test + public void testTypeSchemaOutputWithDescription() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaType.addFieldDefinition(schemaFieldDefinition); + schemaType.description("Type Description"); + + assertThat(schemaType.fieldDefinitions().get(0).equals(schemaFieldDefinition), is(true)); + + assertThat(schemaType.getSchemaAsString(), is("\"Type Description\"\ntype Person {\n" + + "person(\nfilter: String\n): Person!\n" + + "}\n")); + assertThat(schemaType.getGraphQLName(), is("type")); + } + + @Test + public void testTypeStringOutputWith2Arguments() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaFieldDefinition.addArgument(createArgument("age", "Int", true, null, INTEGER)); + schemaType.addFieldDefinition(schemaFieldDefinition); + + assertThat(schemaType.getSchemaAsString(), is("type Person {\n" + + "person(\nfilter: String, \nage: Int!\n): Person!\n" + + "}\n")); + } + + @Test + public void testTypeStringOutputWith2ArgumentsWithArgumentDescriptions() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + SchemaArgument schemaArgument1 = createArgument("filter", "String", false, null, STRING); + schemaArgument1.description("Argument1 Description"); + schemaFieldDefinition.addArgument(schemaArgument1); + + SchemaArgument schemaArgument2 = createArgument("age", "Int", true, null, INTEGER); + schemaArgument2.description("Argument 2 Description"); + schemaFieldDefinition.addArgument(schemaArgument2); + schemaType.addFieldDefinition(schemaFieldDefinition); + + assertThat(schemaType.getSchemaAsString(), + is("type Person {\nperson(\n\"Argument1 Description\"\nfilter: String, \n\"Argument 2 Description\"\nage: " + + "Int!\n)" + + ": Person!\n}\n")); + } + + @Test + public void testTypeInterfaceStringOutputWith2Arguments() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaFieldDefinition.addArgument(createArgument("age", "Int", true, 30, INTEGER)); + schemaType.addFieldDefinition(schemaFieldDefinition); + schemaType.isInterface(true); + assertThat(schemaType.getGraphQLName(), is("interface")); + + assertThat(schemaType.getSchemaAsString(), is("interface Person {\n" + + "person(\nfilter: String, \nage: Int! = 30\n): Person!\n" + + "}\n")); + } + + @Test + public void testTypeInterfaceStringOutputWith2ArgumentsAndStringDefault() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("filter", "String", false, "hello", STRING)); + schemaFieldDefinition.addArgument(createArgument("age", "Int", true, 30, INTEGER)); + schemaType.addFieldDefinition(schemaFieldDefinition); + schemaType.isInterface(true); + assertThat(schemaType.getGraphQLName(), is("interface")); + + assertThat(schemaType.getSchemaAsString(), is("interface Person {\n" + + "person(\nfilter: String = \"hello\", \nage: Int! = 30\n): " + + "Person!\n" + + "}\n")); + } + + @Test + public void testTypeStringOutputWith2Fields() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("personId", "String", true, null, STRING)); + schemaType.addFieldDefinition(schemaFieldDefinition); + + SchemaFieldDefinition schemaFieldDefinition2 = SchemaFieldDefinition.builder() + .name("people") + .returnType("Person") + .arrayReturnType(true) + .returnTypeMandatory(false) + .arrayLevels(1) + .build(); + + schemaFieldDefinition2.addArgument(createArgument("filter", "String", false, null, STRING)); + schemaFieldDefinition2.addArgument(createArgument("age", "Int", true, null, INTEGER)); + schemaType.addFieldDefinition(schemaFieldDefinition2); + + assertThat(schemaType.getSchemaAsString(), is("type Person {\n" + + "person(\npersonId: String!\n): Person!\n" + + "people(\nfilter: String, \nage: Int!\n): [Person]\n" + + "}\n")); + } + + @Test + public void testCreatingInputTypeFromType() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + SchemaFieldDefinition schemaFieldDefinition = SchemaFieldDefinition.builder() + .name("person") + .returnType("Person") + .arrayReturnType(false) + .returnTypeMandatory(true) + .arrayLevels(0) + .build(); + + schemaFieldDefinition.addArgument(createArgument("personId", "String", true, null, STRING)); + schemaType.addFieldDefinition(schemaFieldDefinition); + + SchemaInputType inputType = schemaType.createInputType("Input"); + assertThat(inputType, is(notNullValue())); + assertThat(inputType.fieldDefinitions().size(), is(1)); + assertThat(inputType.fieldDefinitions().get(0).arguments().size(), is(0)); + assertThat(inputType.getSchemaAsString(), is("input PersonInput {\n" + + "person: Person!\n" + + "}\n")); + } + + @Test + public void testToStringInternal() { + SchemaType schemaType = SchemaType.builder() + .name("Person") + .valueClassName("com.oracle.Person") + .build(); + + assertThat(schemaType.toStringInternal(), is(notNullValue())); + assertThat(schemaType.toString(), is(notNullValue())); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/TestHelper.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/TestHelper.java new file mode 100644 index 00000000000..163300f98ed --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/TestHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +/** + * Helpers for tests. + */ +public class TestHelper { + + /** + * Helper to create a {@link SchemaArgument}. + * @param argumentName argument name + * @param argumentType argument type + * @param isMandatory indicates if the argument is mandatory + * @param defaultValue default value + * @param originalType original type + * @return a new {@link SchemaArgument} + */ + public static SchemaArgument createArgument(String argumentName, String argumentType, + boolean isMandatory, Object defaultValue, Class originalType) { + return SchemaArgument.builder() + .argumentName(argumentName) + .argumentType(argumentType) + .mandatory(isMandatory) + .defaultValue(defaultValue) + .originalType(originalType) + .build(); + } + + /** + * Helper to create a {@link SchemaType}. + * @param name name of the type + * @param valueClassName value class name + * @return a new {@link SchemaType} + */ + public static SchemaType createSchemaType(String name, String valueClassName) { + return SchemaType.builder() + .name(name) + .valueClassName(valueClassName) + .build(); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/db/TestDB.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/db/TestDB.java new file mode 100644 index 00000000000..a5874783e1c --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/db/TestDB.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.db; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.types.DateTimePojo; +import io.helidon.microprofile.graphql.server.test.types.NullPOJO; +import io.helidon.microprofile.graphql.server.test.types.TypeWithNameAndJsonbProperty; +import javax.enterprise.context.ApplicationScoped; + +import io.helidon.microprofile.graphql.server.test.types.Address; +import io.helidon.microprofile.graphql.server.test.types.DefaultValuePOJO; +import io.helidon.microprofile.graphql.server.test.types.MultiLevelListsAndArrays; +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +/** + * An injectable datasource for integration tests. + */ +@ApplicationScoped +@SuppressWarnings("unchecked") +public class TestDB { + + /** + * An empty map. + */ + protected static final Map EMPTY_MAP = new HashMap<>(); + + /** + * US Postal Service two letter postal codes. + */ + private static final String[] STATE_CODES = { + "AL", "AK", "AS", "AZ", "AR", "CA", "CO", "CT", "DE", "OF", "DC", + "FM", "FL", "GA", "GU", "HI", "ID", "IL", "IN", "IA", "KS", "KY", + "LA", "ME", "MH", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", + "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "MP", "OH", "OK", "OR", + "PW", "PA", "PR", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VI", + "VA", "WA", "WV", "WI", "WY" + }; + + /** + * Used for generating random dates. + */ + private static Random RANDOM = new Random(); + + /** + * Maximum number people to create. + */ + public static final int MAX_PEOPLE = 1000; + + private final Map allPeople = new HashMap<>(); + + public TestDB() { + for (int i = 1; i <= MAX_PEOPLE; i++) { + Person p = generatePerson(i); + allPeople.put(p.getPersonId(), p); + + } + } + + /** + * Generate a random {@link Person}. + * + * @param personId person id to use + * @return a random {@link Person} + */ + public Person generatePerson(int personId) { + Address homeAddress = generateHomeAddress(); + Address workAddress = generateWorkAddress(); + Address prevAddress1 = generateWorkAddress(); + Address prevAddress2 = generateWorkAddress(); + return new Person(personId, "Person " + personId, homeAddress, workAddress, BigDecimal.valueOf(RANDOM.nextFloat()), + List.of("BA", "BA Hon"), + List.of(prevAddress1, prevAddress2), new int[0], new String[0], EMPTY_MAP, + LocalDate.now(), System.nanoTime(), BigDecimal.valueOf(10)); + } + + public Person getPerson(int personId) { + return allPeople.get(personId); + } + + public Collection getAllPeople() { + return allPeople.values(); + } + + public MultiLevelListsAndArrays getMultiLevelListsAndArrays() { + List> listListBigDecimal = new ArrayList<>(); + listListBigDecimal.add(Collections.singletonList(new BigDecimal(100))); + int[][] intMultiLevelArray = new int[2][]; + intMultiLevelArray[0] = new int[] { 1, 2, 3 }; + intMultiLevelArray[1] = new int[] { 4, 5, 6 }; + Person[][] personMultiLevelArray = new Person[3][]; + personMultiLevelArray[0] = new Person[] { generatePerson(1), generatePerson(2) }; + personMultiLevelArray[1] = new Person[] { generatePerson(3), generatePerson(4) }; + List listOfStringArrays = new ArrayList<>(); + listOfStringArrays.add(new String[] { "one", "two", "three" }); + listOfStringArrays.add(new String[] { "four", "five" }); + String[][][] multiStringArray = { { { "one", "two" }, { "three", "four" } }, + { { "five", "six" }, { "seven", "eight" } } }; + Collection>> colColColString = new ArrayList<>(); + colColColString.add(Collections.singletonList(Collections.singleton("a"))); + + return new MultiLevelListsAndArrays(listListBigDecimal, null, null, intMultiLevelArray, + personMultiLevelArray, listOfStringArrays, multiStringArray, colColColString); + } + + public SimpleContact createRandomContact() { + return new SimpleContact(UUID.randomUUID().toString(), "Name-" + RANDOM.nextInt(10000), + RANDOM.nextInt(100) + 1, EnumTestWithEnumName.XL); + } + + public SimpleContact createContactWithName(String name) { + return new SimpleContact(UUID.randomUUID().toString(), name, RANDOM.nextInt(100) + 1, EnumTestWithEnumName.XL); + } + + /** + * Return a random Zip code. + * + * @return a random Zip code + */ + private static String getRandomZip() { + return String.valueOf(RANDOM.nextInt(99999)); + } + + /** + * Return a random state. + * + * @return a random state + */ + private static String getRandomState() { + return STATE_CODES[RANDOM.nextInt(STATE_CODES.length)]; + } + + /** + * Return a random name. + * + * @return a random name + */ + private static String getRandomName() { + int cCh = 4 + RANDOM.nextInt(7); + char[] ach = new char[cCh]; + + ach[0] = (char) ('A' + RANDOM.nextInt(26)); + for (int of = 1; of < cCh; ++of) { + ach[of] = (char) ('a' + RANDOM.nextInt(26)); + } + return new String(ach); + } + + public Address generateHomeAddress() { + return new Address("1234 Railway Parade", null, getRandomName(), + getRandomState(), getRandomZip(), "US"); + } + + public Address generateWorkAddress() { + return new Address("8 Yawkey Way", null, getRandomName(), + getRandomState(), getRandomZip(), "US"); + } + + public DefaultValuePOJO generatePOJO(String id, int value) { + return new DefaultValuePOJO(id, value); + } + + public TypeWithNameAndJsonbProperty getTypeWithNameAndJsonbProperty() { + return new TypeWithNameAndJsonbProperty("name1", "name2", "name3", "name4", "name5", "name6"); + } + + public DateTimePojo getDateTimePOJO() { + return new DateTimePojo(LocalDate.of(1968, 2, 17), + LocalDate.of(1970, 8, 4), + LocalTime.of(10, 10, 20), + OffsetTime.of(8, 10, 1, 0, ZoneOffset.UTC), + LocalDateTime.now(), OffsetDateTime.now(), ZonedDateTime.now(), LocalDate.now(), + List.of(LocalDate.of(1968, 2, 17), LocalDate.of(1970, 8, 4)), + List.of(LocalDate.of(1968, 2, 17), LocalDate.of(1970, 8, 4))); + } + + public NullPOJO getNullPOJO() { + return new NullPOJO(1, null, "String", List.of("Tim")); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestNoEnumName.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestNoEnumName.java new file mode 100644 index 00000000000..abd0ef6b193 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestNoEnumName.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.enums; + +import org.eclipse.microprofile.graphql.Enum; + +/** + * Class to test enum discovery with no enum name. + */ +@Enum +public enum EnumTestNoEnumName { + S, + M, + L, + XL, + XXL, + XXXL +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithEnumName.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithEnumName.java new file mode 100644 index 00000000000..efba9770503 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithEnumName.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.enums; + +import org.eclipse.microprofile.graphql.Enum; + +/** + * Class to test enum discovery with enum name. + */ +@Enum("TShirtSize") +public enum EnumTestWithEnumName { + S, + M, + L, + XL, + XXL, + XXXL +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAndNameAnnotation.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAndNameAnnotation.java new file mode 100644 index 00000000000..6c6e8e67018 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAndNameAnnotation.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.enums; + +import org.eclipse.microprofile.graphql.Enum; +import org.eclipse.microprofile.graphql.Name; + +/** + * Class to test enum discovery with enum name and name annotation. + * The enum name should win. + */ +@Enum("ThisShouldWin") +@Name("TShirtSize") +public enum EnumTestWithNameAndNameAnnotation { + S, + M, + L, + XL, + XXL, + XXXL +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAnnotation.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAnnotation.java new file mode 100644 index 00000000000..48a68cd3dee --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/enums/EnumTestWithNameAnnotation.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.enums; + +import org.eclipse.microprofile.graphql.Enum; +import org.eclipse.microprofile.graphql.Name; + +/** + * Class to test enum discovery with name annotation. + */ +@Enum +@Name("TShirtSize") +public enum EnumTestWithNameAnnotation { + S, + M, + L, + XL, + XXL, + XXXL +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/exception/ExceptionQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/exception/ExceptionQueries.java new file mode 100644 index 00000000000..1207fee8ff7 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/exception/ExceptionQueries.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.exception; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import java.io.IOException; +import java.security.AccessControlException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.GraphQLException; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + + +/** + * Class that holds queries that raise various exceptions. + */ +@GraphQLApi +@ApplicationScoped +public class ExceptionQueries { + + @Inject + private TestDB testDB; + + public ExceptionQueries() { + } + + @Query + public String query1() { + return "hello world"; + } + + @Query("uncheckedQuery1") + public String uncheckedQuery1() { + throw new IllegalArgumentException(new AccessControlException("my exception")); + } + + @Query("uncheckedQuery2") + public String uncheckedQuery2() { + throw new MyIllegalArgumentException(new AccessControlException("my exception")); + } + + @Query + public String checkedQuery1(@Name("throwException") boolean throwException) throws IOException { + if (throwException) { + throw new IOException("exception"); + } + return String.valueOf(throwException); + } + + @Query("checkedException") + public String checkedException() throws IOException { + throw new IOException("unable to do this"); + } + + @Query("checkedException2") + public String checkedException2() throws MyIOException { + throw new MyIOException("my message"); + } + + @Query("defaultContact") + public SimpleContact getDefaultContact() { + return testDB.createRandomContact(); + } + + @Query + public List failAfterNResults(@Name("failAfter") int failAfter) throws GraphQLException { + List listIntegers = new ArrayList<>(); + int i = 0; + while (i++ < failAfter) { + listIntegers.add(i); + } + throw new GraphQLException("Partial results", listIntegers); + } + + @Query + public List failAfterNContacts(@Name("failAfter") int failAfter) throws GraphQLException { + List listContacts = new ArrayList<>(); + int i = 0; + while (i++ < failAfter) { + listContacts.add(new SimpleContact("id-" + i, "Name-" + i, i, EnumTestWithEnumName.XL)); + } + throw new GraphQLException("Partial results", listContacts); + } + + @Query + public String throwOOME() { + throw new OutOfMemoryError("error"); + } + + public static class MyIOException extends IOException { + public MyIOException() { + super(); + } + + public MyIOException(String message) { + super(message); + } + } + + public static class MyIllegalArgumentException extends IllegalArgumentException { + public MyIllegalArgumentException(Throwable cause) { + super(cause); + } + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/SimpleMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/SimpleMutations.java new file mode 100644 index 00000000000..258b04995d8 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/SimpleMutations.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.mutations; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple mutations definitions with various numbers of arguments. + */ +@GraphQLApi +@ApplicationScoped +public class SimpleMutations { + + public SimpleMutations() { + } + + @Inject + private TestDB testDB; + + @Query + public String helloWorld() { + return "Hello World"; + } + + @Mutation + public SimpleContact createNewContact() { + return testDB.createRandomContact(); + } + + @Mutation("createContactWithName") + public SimpleContact createContact(@Name("name") String name) { + return testDB.createContactWithName(name); + } + + // this method should have then name with the set removed + @Mutation + public String setEchoStringValue(@Name("value") String value) { + return value; + } + + @Mutation + public String testId(@Name("name") String name, @Name("id") Long idNumber) { + return "OK"; + } + + @Mutation("createAndReturnNewContact") + public SimpleContact createNewContact(@Name("newContact") SimpleContact contact) { + return contact; + } + + @Mutation + public String testStringArrays(@Name("places") Set places) { + StringBuilder sb = new StringBuilder(); + places.forEach(sb::append); + return sb.toString(); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/VoidMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/VoidMutations.java new file mode 100644 index 00000000000..cd54d02c7e0 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/mutations/VoidMutations.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.mutations; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; + +/** + * Class that holds mutations that return void. These are not allowed and should cause the server to fail to startup. + */ +@GraphQLApi +@ApplicationScoped +public class VoidMutations { + + public VoidMutations() { + } + + @Mutation + public void badMutation() { + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/ArrayAndListQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/ArrayAndListQueries.java new file mode 100644 index 00000000000..83903dbf810 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/ArrayAndListQueries.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.MultiLevelListsAndArrays; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds array, {@link java.util.Collection} and {@link java.util.Map} queries. + */ +@GraphQLApi +@ApplicationScoped +public class ArrayAndListQueries { + + @Inject + private TestDB testDB; + + public ArrayAndListQueries() { + } + + @Query + @Name("getMultiLevelList") + public MultiLevelListsAndArrays returnLists() { + return testDB.getMultiLevelListsAndArrays(); + } + + @Query("returnListOfStringArrays") + public Collection getListOfStringArrays() { + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new String[] { "one", "two"}); + arrayList.add(new String[] { "three", "four", "five"}); + return arrayList; + } + + @Query + public List echoLinkedListBigDecimals(@Name("param") LinkedList value) { + return value; + } + + @Query + public List echoListBigDecimal(@Name("param") List value) { + return value; + } + + @Query + public BigDecimal[] echoBigDecimalArray(@Name("param") BigDecimal[] value) { + return value; + } + + @Query + public int[] echoIntArray(@Name("param") int[] value) { + return value; + } + + @Query + public short[] echoShortArray(@Name("param") short[] value) { + return value; + } + + @Query + public long[] echoLongArray(@Name("param") long[] value) { + return value; + } + + @Query + public double[] echoDoubleArray(@Name("param") double[] value) { + return value; + } + + @Query + public boolean[] echoBooleanArray(@Name("param") boolean[] value) { + return value; + } + + @Query + public char[] echoCharArray(@Name("param") char[] value) { + return value; + } + + @Query + public float[] echoFloatArray(@Name("param") float[] value) { + return value; + } + + @Query + public byte[] echoByteArray(@Name("param") byte[] value) { + return value; + } + + @Query + public int[][] echo2LevelIntArray(@Name("param") int [][] value) { + return value; + } + + @Query + public SimpleContact[] echoSimpleContactArray(@Name("param") SimpleContact[] value) { + return value; + } + + @Query + public String processListListBigDecimal(@Name("param") List> value) { + return value.toString(); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DateTimeScalarQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DateTimeScalarQueries.java new file mode 100644 index 00000000000..c266ac787a1 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DateTimeScalarQueries.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.bind.annotation.JsonbDateFormat; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.SimpleDateTimePojo; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple query definitions with no-argument. + */ +@GraphQLApi +@ApplicationScoped +public class DateTimeScalarQueries { + + @Inject + private TestDB testDB; + + public DateTimeScalarQueries() { + } + + @Query + public SimpleDateTimePojo echoSimpleDateTimePojo(@Name("dates") List listDates) { + return new SimpleDateTimePojo(listDates); + } + + @Mutation + @JsonbDateFormat("HH:mm") + public LocalTime echoLocalTime(@Name("time") LocalTime localTime) { + return localTime; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DefaultValueQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DefaultValueQueries.java new file mode 100644 index 00000000000..83f8af2f7ab --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DefaultValueQueries.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.DefaultValuePOJO; + +import org.eclipse.microprofile.graphql.DefaultValue; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that work with {@link DefaultValue} annotation. + */ +@GraphQLApi +@ApplicationScoped +public class DefaultValueQueries { + + private static final String DEFAULT_INPUT = "{" + + " \"id\": \"ID-1\"," + + " \"value\": 1000," + + " \"booleanValue\": true," + + " \"dateObject\": \"1968-02-17\"," + + " \"formattedIntWithDefault\": \"2 value\"," + + " \"offsetDateTime\": \"29 Jan 2020 at 09:45 in zone +0200\"" + + "}"; + + @Inject + private TestDB testDB; + + public DefaultValueQueries() { + } + + @Mutation + @Name("generateDefaultValuePOJO") + public DefaultValuePOJO createNewPJO(@Name("id") @DefaultValue("ID-1") String id, + @Name("value") @DefaultValue("1000") int value) { + return testDB.generatePOJO(id, value); + } + + @Query + @Name("echoDefaultValuePOJO") + public DefaultValuePOJO echo(@Name("input") @DefaultValue(DEFAULT_INPUT) DefaultValuePOJO input) { + return input; + } + + @Query + @Name("echoDefaultValuePOJO2") + public DefaultValuePOJO echo2(@Name("input") DefaultValuePOJO input) { + return input; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DescriptionQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DescriptionQueries.java new file mode 100644 index 00000000000..5e6a5946cf2 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DescriptionQueries.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import io.helidon.microprofile.graphql.server.test.types.DescriptionType; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that work with types with various descriptions. + */ +@GraphQLApi +@ApplicationScoped +public class DescriptionQueries { + + public DescriptionQueries() { + } + + @Query + public DescriptionType retrieveType() { + return new DescriptionType("ID1", 10); + } + + @Query + public boolean validateType(@Name("type") DescriptionType type) { + return true; + } + + @Query + @Name("descriptionOnParam") + public boolean descriptionOnParam(@Name("param1") @Description("Description for param1") String param1) { + return true; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DuplicateNameQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DuplicateNameQueries.java new file mode 100644 index 00000000000..9e8025dde67 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/DuplicateNameQueries.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that have duplicate names. + */ +@GraphQLApi +@ApplicationScoped +public class DuplicateNameQueries { + + public DuplicateNameQueries() { + } + + @Query("myQuery") + public String myQuery1() { + return null; + } + + @Mutation("myQuery") + public String myQuery2() { + return null; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/InvalidQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/InvalidQueries.java new file mode 100644 index 00000000000..ff6c1dffb7e --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/InvalidQueries.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that also have Mutation annotation. + */ +@GraphQLApi +@ApplicationScoped +public class InvalidQueries { + + public InvalidQueries() { + } + + @Query + @Mutation + public void badQuery() { + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/MapQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/MapQueries.java new file mode 100644 index 00000000000..d95484dab9e --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/MapQueries.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import java.util.Map; +import java.util.TreeMap; + +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.graphql.server.test.types.TypeWithMap; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that have {@link Map} as return values or types with {@link Map}s. + */ +@GraphQLApi +@ApplicationScoped +public class MapQueries { + + @Query("query1") + public TypeWithMap generateTypeWithMap() { + Map mapValues = new TreeMap<>(); + mapValues.put(1, "one"); + mapValues.put(2, "two"); + + Map mapContacts = new TreeMap<>(); + + SimpleContact contact1 = new SimpleContact("c1", "Tim", 55, EnumTestWithEnumName.XL); + SimpleContact contact2 = new SimpleContact("c2", "James", 75, EnumTestWithEnumName.XXL); + mapContacts.put(contact1.getId(), contact1); + mapContacts.put(contact2.getId(), contact2); + return new TypeWithMap("id1", mapValues, mapContacts); + } + + @Query("query2") + public TypeWithMap echoTypeWithMap(@Name("value") TypeWithMap value) { + return value; + } + + @Query("query3") + public Map thisShouldRaiseError(@Name("value") Map value) { + return value; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NoopQueriesAndMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NoopQueriesAndMutations.java new file mode 100644 index 00000000000..d3cc72189fe --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NoopQueriesAndMutations.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Queries to avoid extra validation from 16.1 graphql-java release. + */ +@GraphQLApi +public class NoopQueriesAndMutations { + + @Query + public String helloWorld() { + return "Hello World"; + } + + @Mutation + public String echo(@Name("input") String input) { + return input; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NumberFormatQueriesAndMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NumberFormatQueriesAndMutations.java new file mode 100644 index 00000000000..fd805d67e9f --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/NumberFormatQueriesAndMutations.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithNumberFormats; +import javax.json.bind.annotation.JsonbNumberFormat; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries and mutations that have various formatting types. + */ +@GraphQLApi +@ApplicationScoped +public class NumberFormatQueriesAndMutations { + + public NumberFormatQueriesAndMutations() { + } + + @Query("simpleFormattingQuery") + public SimpleContactWithNumberFormats retrieveFormattedObject() { + return new SimpleContactWithNumberFormats(1, "Tim", 50, 1200.0f, 10, Long.MAX_VALUE, BigDecimal.valueOf(100)); + } + + @Mutation("generateDoubleValue") + @NumberFormat("Double-###########") + public Double generateDouble() { + return 123456789d; + } + + @Mutation("createSimpleContactWithNumberFormats") + public SimpleContactWithNumberFormats createContact(@Name("contact") SimpleContactWithNumberFormats contact) { + return contact; + } + + @Query + public int echoNumberUnformatted(@Name("number") @NumberFormat("ID-#########") int number) { + return number; + } + + @Query + public BigDecimal echoBigDecimalUsingFormat(@Name("param1") @NumberFormat("BD-####") BigDecimal param1) { + return param1; + } + + @Query + public List getListAsString(@Name("arg1") + // this should be ignored as NumberFormat is Below + @JsonbNumberFormat("ignore 00.0000000") + List<@NumberFormat("'value' 00.0000000") BigDecimal> values) { + if (values != null) { + return values.stream().map(Object::toString).collect(Collectors.toList()); + } + + return new ArrayList<>(); + } + + @Mutation + public Double echoBankBalance(@JsonbNumberFormat(value = "¤ ###,###.##", locale = "en-US") + @Name("bankBalance") Double bankBalance) { + return bankBalance; + } + + @Mutation + public float echoFloat(@Name("size") float size) { + return size; + } + + @Mutation + @NumberFormat(value = "number #", locale = "en-GB") + public Integer transformedNumber(Integer input) { + return input; + } + + @Mutation + public String idNumber(@Name("name") String name, + @Name("id") Long idNumber) { + return name + "-" + idNumber; + } + + @Mutation + public List echoBigDecimalList(@Name("coordinates") List coordinates) { + return coordinates; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/OddNamedQueriesAndMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/OddNamedQueriesAndMutations.java new file mode 100644 index 00000000000..282bbf9750a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/OddNamedQueriesAndMutations.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries and mutation with odd names. + */ +@GraphQLApi +@ApplicationScoped +public class OddNamedQueriesAndMutations { + + public OddNamedQueriesAndMutations() { + } + + @Query + public String settlement() { + return "this should be ok"; + } + + @Mutation + public String getaway() { + return "let's getaway"; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/PropertyNameQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/PropertyNameQueries.java new file mode 100644 index 00000000000..235502a9ca0 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/PropertyNameQueries.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.TypeWithNameAndJsonbProperty; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple query definitions with types that have different property + * names through {@link javax.json.bind.annotation.JsonbProperty} or {@link Name}. + */ +@GraphQLApi +@ApplicationScoped +public class PropertyNameQueries { + + @Inject + private TestDB testDB; + + public PropertyNameQueries() { + } + + @Query("query1") + public TypeWithNameAndJsonbProperty getDefaultType() { + return testDB.getTypeWithNameAndJsonbProperty(); + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesAndMutationsWithNulls.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesAndMutationsWithNulls.java new file mode 100644 index 00000000000..982c754dbec --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesAndMutationsWithNulls.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.NullPOJO; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import java.time.LocalDate; + +import org.eclipse.microprofile.graphql.DefaultValue; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Query; + + +/** + * Class that holds queries that also have Null. + */ +@GraphQLApi +@ApplicationScoped +public class QueriesAndMutationsWithNulls { + + @Inject + private TestDB testDB; + + public QueriesAndMutationsWithNulls() { + } + + @Query("method1NotNull") + @NonNull + public String getAString() { + return "A String"; + } + + @Query("method2NotNull") + public int getInt() { + return 1; + } + + @Query("method3NotNull") + public Long getLong() { + return 1L; + } + + // should be optional + @Query("paramShouldBeNonMandatory") + public String query1(@Name("value") String value) { + return "value"; + } + + // should be optional as even though it's primitive, there is DefaultValue + @Query("paramShouldBeNonMandatory2") + public String query2(@Name("value") @DefaultValue("1") int value) { + return "value"; + } + + // even though we have NonNull, there is a default value so should be optional + @Query("paramShouldBeNonMandatory3") + public String query3(@Name("value") @DefaultValue("value") @NonNull String value) { + return "value"; + } + + // just to generate NullPOJOInput + @Query + public boolean validate(@Name("pojo") NullPOJO nullPOJO) { + return false; + } + + // argument should be mandatory + @Query + public String testMandatoryArgument(@Name("arg1") int arg1) { + return null; + } + + @Mutation("returnNullValues") + public NullPOJO getNullPOJO() { + return testDB.getNullPOJO(); + } + + @Mutation("echoNullValue") + public String echoNullValue(String value) { + return value; + } + + @Mutation("echoNullDateValue") + public LocalDate echoNullDateValue(LocalDate value) { + return value; + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesWithIgnorable.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesWithIgnorable.java new file mode 100644 index 00000000000..96d348f67e2 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/QueriesWithIgnorable.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.ObjectWithIgnorableFieldsAndMethods; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple query definitions with types that have ignorable methods/fields. + */ +@GraphQLApi +@ApplicationScoped +public class QueriesWithIgnorable { + + @Inject + private TestDB testDB; + + public QueriesWithIgnorable() { + } + + @Query("testIgnorableFields") + public ObjectWithIgnorableFieldsAndMethods getIgnorable() { + return new ObjectWithIgnorableFieldsAndMethods("id", "string", 1, true, 101); + } + + @Query("generateInputType") + public boolean canFind(@Name("input") ObjectWithIgnorableFieldsAndMethods ignorable) { + return false; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesAndMutations.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesAndMutations.java new file mode 100644 index 00000000000..2d8af5f48ac --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesAndMutations.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.bind.annotation.JsonbDateFormat; +import javax.json.bind.annotation.JsonbProperty; + +import io.helidon.microprofile.graphql.server.test.types.SimpleDateTime; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.types.DateTimePojo; +import io.helidon.microprofile.graphql.server.test.types.MultiLevelListsAndArrays; +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithSelf; +import io.helidon.microprofile.graphql.server.test.types.TypeWithIDs; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple query definitions with no-argument. + */ +@GraphQLApi +@ApplicationScoped +public class SimpleQueriesAndMutations { + + @Inject + private TestDB testDB; + + public SimpleQueriesAndMutations() { + } + + @Mutation + public SimpleDateTime echoSimpleDateTime(@Name("value") SimpleDateTime simpleDateTime) { + return simpleDateTime; + } + + @Query + public Boolean isBooleanObject() { + return Boolean.valueOf(true); + } + + @Query + public boolean isBooleanPrimitive() { + return false; + } + + @Query + @Name("idQuery") + @Id + public String returnId() { + return "an-id"; + } + + @Query + public String hero() { + return "R2-D2"; + } + + @Query + @Name("episodeCount") + public int getNumberOfEpisodes() { + return 9; + } + + @Query + @JsonbProperty("numberOfStars") + public Long getTheNumberOfStars() { + return Long.MAX_VALUE; + } + + @Query("badGuy") + public String getVillain() { + return "Darth Vader"; + } + + @Query("allPeople") + public Collection findAllPeople() { + return testDB.getAllPeople(); + } + + @Query + public LocalDate returnCurrentDate() { + return LocalDate.now(); + } + + @Query + @Name("returnMediumSize") + public EnumTestWithEnumName getEnumMedium() { + return EnumTestWithEnumName.M; + } + + @Query + public TypeWithIDs returnTypeWithIDs() { + return new TypeWithIDs(1, 2, "string", 10L, 10L, UUID.randomUUID()); + } + + @Query + public SimpleContactWithSelf returnSimpleContactWithSelf() { + SimpleContactWithSelf spouse = new SimpleContactWithSelf("c1", "contact1", 30); + SimpleContactWithSelf contact = new SimpleContactWithSelf("c2", "contact2", 33); + contact.setSpouse(spouse); + return contact; + } + + @Query + @Name("getMultiLevelList") + public MultiLevelListsAndArrays returnLists() { + List> listListBigDecimal = new ArrayList<>(); + listListBigDecimal.add(Collections.singletonList(new BigDecimal(100))); + int[][] intMultiLevelArray = new int[2][]; + intMultiLevelArray[0] = new int[] { 1, 2, 3 }; + intMultiLevelArray[1] = new int[] { 4, 5, 6 }; + Person[][] personMultiLevelArray = new Person[3][]; + personMultiLevelArray[0] = new Person[] { testDB.generatePerson(1), testDB.generatePerson(2) }; + personMultiLevelArray[1] = new Person[] { testDB.generatePerson(3), testDB.generatePerson(4) }; + List listOfStringArrays = new ArrayList<>(); + listOfStringArrays.add(new String[] { "one", "two", "three" }); + listOfStringArrays.add(new String[] { "four", "five" }); + String[][][] multiStringArray = { { { "one", "two" }, { "three", "four" } }, + { { "five", "six" }, { "seven", "eight" } } }; + Collection>> colColColString = new ArrayList<>(); + colColColString.add(Collections.singletonList(Collections.singleton("a"))); + + return new MultiLevelListsAndArrays(listListBigDecimal, null, null, intMultiLevelArray, + personMultiLevelArray, listOfStringArrays, multiStringArray, colColColString); + } + + @Query("dateAndTimePOJOQuery") + public DateTimePojo dateTimePojo() { + return testDB.getDateTimePOJO(); + } + + @Query("localDateListFormat") + public List<@DateFormat("dd/MM/yyyy") LocalDate> getLocalDateListFormat() { + return List.of(LocalDate.of(1968, 2, 17), LocalDate.of(1970, 8, 4)); + } + + @Query + @DateFormat(value = "dd MMM yyyy", locale = "en-AU") + public LocalDate transformedDate() { + String date = "2016-08-16"; + return LocalDate.parse(date); + } + + @Query + public DateTimePojo echoDateTimePojo(@Name("value") DateTimePojo dateTimePojo) { + return dateTimePojo; + } + + @Query("localDateNoFormat") + public LocalDate localDateNoFormat() { + return LocalDate.of(1968, 02, 17); + } + + @Mutation + public DateTimePojo dateTimePojoMutation() { + return testDB.getDateTimePOJO(); + } + + @Mutation + public LocalDate echoLocalDate(@DateFormat("dd/MM/yyyy") @Name("dateArgument") LocalDate date) { + return date; + } + + @Mutation + @DateFormat(value = "dd MMM yyyy", locale = "en-GB") + public LocalDate echoLocalDateGB(@DateFormat(value = "dd/MM/yyyy") @Name("dateArgument") LocalDate date) { + return date; + } + + @Query + @DateFormat(value = "dd MMM yyyy", locale = "en-GB") + public LocalDate queryLocalDateGB() { + return LocalDate.of(1968, 2, 17); + } + + @Query + @DateFormat(value = "dd MMM yyyy", locale = "en-AU") + public LocalDate queryLocalDateAU() { + return LocalDate.of(1968, 2, 17); + } + + @Mutation + @DateFormat(value = "dd MMM yyyy", locale = "en-AU") + public LocalDate echoLocalDateAU(@DateFormat(value = "dd/MM/yyyy") @Name("dateArgument") LocalDate date) { + return date; + } + + @Mutation + @JsonbDateFormat("HH:mm:ss dd-MM-yyyy") + public LocalDateTime testDefaultFormatLocalDateTime(@Name("dateTime") LocalDateTime localDateTime) { + return localDateTime; + } + + @Mutation + @JsonbDateFormat("HH:mm") + public LocalTime echoLocalTime(@Name("time") LocalTime localTime) { + return localTime; + } + + @Query + @JsonbDateFormat("dd/MM") + public List echoFormattedLocalDateWithReturnFormat(@Name("value") List<@DateFormat("dd-MM-yyyy") LocalDate> value) { + return value; + } + + @Mutation + @DateFormat("dd/MM/yyyy") + public List echoFormattedDateWithJsonB(@Name("dates") + @JsonbDateFormat("yy dd MM") // should be ignored + List<@DateFormat("MM/dd/yyyy") LocalDate> localDates) { + return localDates; + } + + @Query + public OffsetDateTime echoOffsetDateTime(@Name("value") + @JsonbDateFormat(value = "dd MMM yyyy 'at' HH:mm 'in zone' Z",locale = "en-ZA") + OffsetDateTime offsetDateTime) { + return offsetDateTime; + } + + @Query + public ZonedDateTime echoZonedDateTime(@Name("value") + @JsonbDateFormat(value = "dd MMMM yyyy 'at' HH:mm 'in' VV",locale = "en-ZA") + ZonedDateTime zonedDateTime) { + return zonedDateTime; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithArgs.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithArgs.java new file mode 100644 index 00000000000..7b9c1d599c7 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithArgs.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import io.helidon.microprofile.graphql.server.test.types.Task; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.bind.annotation.JsonbNumberFormat; +import javax.json.bind.annotation.JsonbProperty; + +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithNumberFormats; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithNameAnnotation; +import io.helidon.microprofile.graphql.server.test.types.ContactRelationship; +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds simple query definitions with various numbers of arguments. + */ +@GraphQLApi +@ApplicationScoped +public class SimpleQueriesWithArgs { + + public SimpleQueriesWithArgs() { + } + + @Inject + private TestDB testDB; + + // tests for ID + @Query + public Integer returnIntegerAsId(@Name("param1") @Id Integer value) { + return value; + } + + @Query + public int returnIntPrimitiveAsId(@Name("param1") @Id int value) { + return value; + } + + @Query + public int returnIntPrimitiveAsIdWithFormat(@JsonbNumberFormat("0 'hello'") @Name("param1") @Id int value) { + return value; + } + + @Query + public Integer returnIntegerAsIdWithFormat(@NumberFormat("0 'format'") @Name("param1") @Id Integer value) { + return value; + } + + @Query + public Integer returnIntAsId(@Name("param1") @Id int value) { + return value; + } + + @Query + public String returnStringAsId(@Name("param1") @Id String value) { + return value; + } + + @Query + public Long returnLongAsId(@Name("param1") @Id Long value) { + return value; + } + + @Query + public Long returnLongAsIdWithFormat(@NumberFormat("#######-Long") @Name("param1") @Id Long value) { + return value; + } + + @Query + public long returnLongPrimitiveAsId(@Name("param1") @Id long value) { + return value; + } + + @Query + public long returnLongPrimitiveAsIdWithFormat(@NumberFormat("#######-long") @Name("param1") @Id long value) { + return value; + } + + @Query + public UUID returnUUIDAsId(@Name("param1") @Id UUID value) { + return value; + } + + @Query + public String echoString(@Name("String") String value) { + return value; + } + + @Query + public int echoInt(@Name("value") int value) { + return value; + } + + @Query + public Integer echoIntegerObject(@Name("value") Integer value) { + return value; + } + + @Query + public int echoIntWithFormat(@NumberFormat("0 'value'") @Name("value") int value) { + return value; + } + + @Query + public Integer echoIntegerObjectWithFormat(@NumberFormat("0 'value'") @Name("value") Integer value) { + return value; + } + + @Query + public double echoDouble(@Name("value") double value) { + return value; + } + + @Query + public double echoDoubleWithFormat(@NumberFormat("#####-format") @Name("value") double value) { + return value; + } + + @Query + public Double echoDoubleObjectWithFormat(@NumberFormat("#####-format") @Name("value") Double value) { + return value; + } + + @Query + public Double echoDoubleObject(@Name("value") Double value) { + return value; + } + + @Query + public float echoFloat(@Name("value") float value) { + return value; + } + + @Query + public float echoFloatWithFormat(@JsonbNumberFormat(value = "¤ 000.00", locale = "en-AU") + @Name("value") float value) { + return value; + } + + @Query + public Float echoFloatObjectWithFormat(@JsonbNumberFormat(value = "¤ 000.00", locale = "en-AU") + @Name("value") Float value) { + return value; + } + + @Query + public Float echoFloatObject(@Name("value") Float value) { + return value; + } + + @Query + public byte echoByte(@Name("value") byte value) { + return value; + } + + @Query + public Byte echoByteObject(@Name("value") Byte value) { + return value; + } + + @Query + public long echoLong(@Name("value") long value) { + return value; + } + + @Query + public long echoLongWithFormat(@NumberFormat("Long-##########") @Name("value") long value) { + return value; + } + + @Query + public Long echoLongObjectWithFormat(@NumberFormat("Long-##########") @Name("value") Long value) { + return value; + } + + @Query + public Long echoLongObject(@Name("value") Long value) { + return value; + } + + @Query + public boolean echoBoolean(@Name("value") boolean value) { + return value; + } + + @Query + public Boolean echoBooleanObject(@Name("value") Boolean value) { + return value; + } + + @Query + public char echoChar(@Name("value") char value) { + return value; + } + + @Query + public Character echoCharacterObject(@Name("value") Character value) { + return value; + } + + @Query + public BigDecimal echoBigDecimal(@Name("value") BigDecimal value) { + return value; + } + + @Query + public BigDecimal echoBigDecimalWithFormat(@NumberFormat("######.##-BigDecimal") @Name("value") BigDecimal value) { + return value; + } + + @Query + public BigInteger echoBigInteger(@Name("value") BigInteger value) { + return value; + } + + @Query + public BigInteger echoBigIntegerWithFormat(@NumberFormat("######-BigInteger") @Name("value") BigInteger value) { + return value; + } + + @Query + public String hero(@Name("heroType") String heroType) { + return "human".equalsIgnoreCase(heroType) + ? "Luke" + : "R2-D2"; + } + + @Query("multiply") + public long multiplyFunction(int number1, int number2) { + return number1 * number2; + } + + @Query + @Name("findAPerson") + public Person findPerson(@Name("personId") int personId) { + return testDB.getPerson(personId); + } + + @Query + public Collection findPeopleFromState(@Name("state") String state) { + return testDB.getAllPeople() + .stream().filter(p -> p.getHomeAddress().getState().equals(state)) + .collect(Collectors.toList()); + } + + @Query + public List findLocalDates(@Name("numberOfValues") int count) { + List localDates = new ArrayList<>(); + for (int i = 0; i < count; i++) { + localDates.add(LocalDate.now()); + } + return localDates; + } + + @Query("getMonthFromDate") + public String returnDateAsLong(@Name("date") LocalDate localDate) { + return localDate.getMonth().toString(); + } + + @Query + public Collection findOneEnum(@Name("enum") EnumTestWithNameAnnotation enum1) { + return Collections.singleton(enum1); + } + + @Query + public boolean canFindContact(@Name("contact") SimpleContact contact) { + return false; + } + + @Query + @JsonbProperty("findEnums") + public String findEnumValue(EnumTestWithNameAnnotation enum1) { + return enum1.name(); + } + + @Query("canFindContactRelationship") + public boolean getContactRelationships(@Name("relationship") ContactRelationship relationship) { + return false; + } + + @Query("additionQuery") + public int addNumbers(@Name("value1") int value1, @Name("value2") int value2) { + return value1 + value2; + } + + @Query + public boolean canFindSimpleContactWithNumberFormats(@Name("contact") SimpleContactWithNumberFormats contact) { + return false; + } + + @Query + public List echoListOfStrings(@Name("value") List value) { + return value; + } + + @Query + public List echoListOfIntegers(@Name("value") List value) { + return value; + } + + @Query + public List echoListOfBigIntegers(@Name("value") List value) { + return value; + } + + @Query + public List echoListOfSimpleContacts(@Name("value") List value) { + return value; + } + + @Query + public Collection echoCollectionOfSimpleContacts(@Name("value") Collection value) { + return value; + } + + @Query + public String[] echoStringArray(@Name("value") String[] value) { + return value; + } + + @Query + public int[] echoIntArray(@Name("value") int[] value) { + return value; + } + + @Query + public Boolean[] echoBooleanArray(@Name("value") Boolean[] value) { + return value; + } + + @Query + public String[][] echoStringArray2(@Name("value") String[][] value) { + return value; + } + + @Query + public SimpleContact[] echoSimpleContactArray(@Name("value") SimpleContact[] value) { + return value; + } + + @Query + public List echoFormattedListOfIntegers(@Name("value") List<@NumberFormat("0 'years old'") Integer> value) { + return value; + } + + @Query + public List echoFormattedLocalDate(@Name("value") List<@DateFormat("dd-MM-yyyy") LocalDate> value) { + return value; + } + + @Mutation + public Task createTask(@Name("task") Task task) { + task = new Task(task.getDescription()); + System.out.println(task); + return task; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithSource.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithSource.java new file mode 100644 index 00000000000..39746847f6a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/SimpleQueriesWithSource.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.Address; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; + +/** + * Class that holds simple query definitions with arguments with the . + */ +@GraphQLApi +@ApplicationScoped +public class SimpleQueriesWithSource { + + public SimpleQueriesWithSource() { + } + + @Inject + private TestDB testDB; + + // the following should add a "idAndName" field on the SimpleContact type + // which will return the id and name concatenated + public String idAndName(@Source @Name("contact") SimpleContact contact) { + return contact.getId() + " " + contact.getName(); + } + + // the following should add a "currentJob" field on the SimpleContact type and a top level query as well + @Query + public String getCurrentJob(@Source @Name("contact") SimpleContact contact) { + return "Manager " + contact.getId(); + } + + // technically this is a mutation but just treading as returning random contact + @Query + @Name("findContact") + public SimpleContact retrieveSimpleContact() { + return testDB.createRandomContact(); + } + + @Name("lastAddress") + public Address returnTheLastAddress(@Source @Name("contact") SimpleContact contact) { + return testDB.generateWorkAddress(); + } + + @Name("lastNAddress") + public Collection
returnTheLastNAddress(@Source @Name("contact") SimpleContact contact, @Name("count") int count) { + Set
setAddresses = new HashSet<>(); + setAddresses.add(testDB.generateWorkAddress()); + setAddresses.add(testDB.generateHomeAddress()); + return setAddresses; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/VoidQueries.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/VoidQueries.java new file mode 100644 index 00000000000..4cce786175f --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/queries/VoidQueries.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.queries; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +/** + * Class that holds queries that return void. These are not allowed and should cause the server to fail to startup. + */ +@GraphQLApi +@ApplicationScoped +public class VoidQueries { + + public VoidQueries() { + } + + @Query + public void badQuery() { + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/AbstractVehicle.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/AbstractVehicle.java new file mode 100644 index 00000000000..91b8477d45b --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/AbstractVehicle.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Abstract implementation of a {@link Vehicle}. + */ +public abstract class AbstractVehicle implements Vehicle { + private String plate; + private int numberOfWheels; + private String model; + private String make; + private int getManufactureYear; + private Collection incidents; + + public AbstractVehicle(String plate, int numberOfWheels, String model, String make, int getManufactureYear) { + this.plate = plate; + this.numberOfWheels = numberOfWheels; + this.model = model; + this.make = make; + this.getManufactureYear = getManufactureYear; + this.incidents = new ArrayList<>(); + } + + @Override + public Collection getIncidents() { + return incidents; + } + + @Override + public String getPlate() { + return plate; + } + + @Override + public int getNumberOfWheels() { + return numberOfWheels; + } + + @Override + public String getMake() { + return make; + } + + @Override + public String getModel() { + return model; + } + + @Override + public int getManufactureYear() { + return getManufactureYear; + } + + public void setPlate(String plate) { + this.plate = plate; + } + + public void setNumberOfWheels(int numberOfWheels) { + this.numberOfWheels = numberOfWheels; + } + + public void setModel(String model) { + this.model = model; + } + + public void setMake(String make) { + this.make = make; + } + + public void setGetManufactureYear(int getManufactureYear) { + this.getManufactureYear = getManufactureYear; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Address.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Address.java new file mode 100644 index 00000000000..2df98e300c1 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Address.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +/** + * Class representing an Address. + */ +public class Address { + private String addressLine1; + private String addressLine2; + private String city; + private String state; + private String zipCode; + private String country; + + public Address(String addressLine1, String addressLine2, String city, String state, String zipCode, String country) { + this.addressLine1 = addressLine1; + this.addressLine2 = addressLine2; + this.city = city; + this.state = state; + this.zipCode = zipCode; + this.country = country; + } + + public String getAddressLine1() { + return addressLine1; + } + + public void setAddressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + } + + public String getAddressLine2() { + return addressLine2; + } + + public void setAddressLine2(String addressLine2) { + this.addressLine2 = addressLine2; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + @Override + public String toString() { + return "Address{" + + "addressLine1='" + addressLine1 + '\'' + + ", addressLine2='" + addressLine2 + '\'' + + ", city='" + city + '\'' + + ", state='" + state + '\'' + + ", zipCode='" + zipCode + '\'' + + ", country='" + country + '\'' + '}'; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Car.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Car.java new file mode 100644 index 00000000000..786d189f673 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Car.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Description; + +/** + * Represents a Car. + */ +@Description("A representation of a car.\nThis description should be surrounded by triple quotes.\nThe end.") +public class Car extends AbstractVehicle { + + private int numberOfDoors; + + public Car(String plate, int numberOfWheels, String model, String make, int getManufactureYear, int numberOfDoors) { + super(plate, numberOfWheels, model, make, getManufactureYear); + this.numberOfDoors = numberOfDoors; + } + + public int getNumberOfDoors() { + return numberOfDoors; + } + + public void setNumberOfDoors(int numberOfDoors) { + this.numberOfDoors = numberOfDoors; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ContactRelationship.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ContactRelationship.java new file mode 100644 index 00000000000..cf723635f5a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ContactRelationship.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Objects; + +/** + * Defines a relationship between two {@link SimpleContact}s. This is to test + * generation of nested InputTypes if this type is used as a parameter. + */ +public class ContactRelationship { + private SimpleContact contact1; + private SimpleContact contact2; + private String relationship; + + public ContactRelationship(SimpleContact contact1, + SimpleContact contact2, String relationship) { + this.contact1 = contact1; + this.contact2 = contact2; + this.relationship = relationship; + } + + public ContactRelationship() { + } + + public SimpleContact getContact1() { + return contact1; + } + + public void setContact1(SimpleContact contact1) { + this.contact1 = contact1; + } + + public SimpleContact getContact2() { + return contact2; + } + + public void setContact2(SimpleContact contact2) { + this.contact2 = contact2; + } + + public String getRelationship() { + return relationship; + } + + public void setRelationship(String relationship) { + this.relationship = relationship; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContactRelationship that = (ContactRelationship) o; + return Objects.equals(contact1, that.contact1) && + Objects.equals(contact2, that.contact2) && + Objects.equals(relationship, that.relationship); + } + + @Override + public int hashCode() { + return Objects.hash(contact1, contact2, relationship); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DateTimePojo.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DateTimePojo.java new file mode 100644 index 00000000000..9153ffb7571 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DateTimePojo.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.List; + +import javax.json.bind.annotation.JsonbDateFormat; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class representing various date/time types. + */ +@Type +public class DateTimePojo { + + @JsonbDateFormat("MM/dd/yyyy") + private LocalDate localDate; + + @DateFormat("MM/dd/yyyy") + private LocalDate localDate2; + + @DateFormat("hh:mm[:ss]") + private LocalTime localTime; + + private OffsetTime offsetTime; + + private LocalDateTime localDateTime; + + private OffsetDateTime offsetDateTime; + + private ZonedDateTime zonedDateTime; + @Description("description") + private LocalDate localDateNoFormat; + + private LocalTime localTimeNoFormat; + + private List significantDates; + + private List formattedListOfDates; + + public DateTimePojo() { + } + + public DateTimePojo(LocalDate localDate, + LocalDate localDate2, + LocalTime localTime, + OffsetTime offsetTime, + LocalDateTime localDateTime, + OffsetDateTime offsetDateTime, + ZonedDateTime zonedDateTime, + LocalDate localDateNoFormat, + List significantDates, + List formattedListOfDates) { + this.localDate = localDate; + this.localDate2 = localDate2; + this.localTime = localTime; + this.offsetTime = offsetTime; + this.localDateTime = localDateTime; + this.offsetDateTime = offsetDateTime; + this.zonedDateTime = zonedDateTime; + this.localDateNoFormat = localDateNoFormat; + this.significantDates = significantDates; + this.formattedListOfDates = formattedListOfDates; + } + + public LocalTime getLocalTimeNoFormat() { + return localTimeNoFormat; + } + + public void setLocalTimeNoFormat(LocalTime localTimeNoFormat) { + this.localTimeNoFormat = localTimeNoFormat; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public OffsetTime getOffsetTime() { + return offsetTime; + } + + public void setOffsetTime(OffsetTime offsetTime) { + this.offsetTime = offsetTime; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public LocalDate getLocalDate2() { + return localDate2; + } + + public void setLocalDate2(LocalDate localDate2) { + this.localDate2 = localDate2; + } + + public LocalDate getLocalDateNoFormat() { + return localDateNoFormat; + } + + public void setLocalDateNoFormat(LocalDate localDateNoFormat) { + this.localDateNoFormat = localDateNoFormat; + } + + public void setSignificantDates(List significantDates) { + this.significantDates = significantDates; + } + + public List getSignificantDates() { + return this.significantDates; + } + + public List<@DateFormat("dd/MM") LocalDate> getFormattedListOfDates() { + return formattedListOfDates; + } + + public void setFormattedListOfDates(List formattedListOfDates) { + this.formattedListOfDates = formattedListOfDates; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DefaultValuePOJO.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DefaultValuePOJO.java new file mode 100644 index 00000000000..3d4207880cb --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DefaultValuePOJO.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; +import java.time.OffsetDateTime; + +import javax.json.bind.annotation.JsonbDateFormat; +import org.eclipse.microprofile.graphql.DefaultValue; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test default values. + */ +@Type +public class DefaultValuePOJO { + + @DefaultValue("ID-123") + private String id; + + private int value; + + @DefaultValue("1 value") + @NumberFormat("0 'value'") + private int formattedIntWithDefault; + + @DefaultValue("1978-07-03") + private LocalDate dateObject; + + @DefaultValue("false") + boolean booleanValue; + + @JsonbDateFormat(value = "dd MMM yyyy 'at' HH:mm 'in zone' Z", locale = "en-ZA") + private OffsetDateTime offsetDateTime; + + public DefaultValuePOJO() { + } + + public DefaultValuePOJO(String id, int value) { + this.id = id; + this.value = value; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getValue() { + return value; + } + + @DefaultValue("111222") + public void setValue(int value) { + this.value = value; + } + + public LocalDate getDateObject() { + return dateObject; + } + + public void setDateObject(LocalDate dateObject) { + this.dateObject = dateObject; + } + + public boolean isBooleanValue() { + return booleanValue; + } + + public void setBooleanValue(boolean booleanValue) { + this.booleanValue = booleanValue; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public int getFormattedIntWithDefault() { + return formattedIntWithDefault; + } + + public void setFormattedIntWithDefault(int formattedIntWithDefault) { + this.formattedIntWithDefault = formattedIntWithDefault; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DescriptionType.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DescriptionType.java new file mode 100644 index 00000000000..d41dc551ecf --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/DescriptionType.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; + +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test descriptions. + */ +@Type +public class DescriptionType { + + @Description("this is the description") + private String id; + private int value; + + @NumberFormat("L-########") + private Long longValue1; + + @NumberFormat(value = "###,###", locale = "en-AU") + @Description("Description") + private Long longValue2; + + private LocalDate localDate; + + public DescriptionType(String id, int value) { + this.id = id; + this.value = value; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Description("description of value") + public int getValue() { + return value; + } + + @Description("description on set for input") + public void setValue(int value) { + this.value = value; + } + + public Long getLongValue1() { + return longValue1; + } + + public void setLongValue1(Long longValue1) { + this.longValue1 = longValue1; + } + + public Long getLongValue2() { + return longValue2; + } + + public void setLongValue2(Long longValue2) { + this.longValue2 = longValue2; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InnerClass.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InnerClass.java new file mode 100644 index 00000000000..07a17194485 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InnerClass.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test inner classes + */ +@Type +public class InnerClass { + + private String id; + private InnerClass innerClass; + + public InnerClass(String id, InnerClass innerClass) { + this.id = id; + this.innerClass = innerClass; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public InnerClass getInnerClass() { + return innerClass; + } + + public void setInnerClass(InnerClass innerClass) { + this.innerClass = innerClass; + } + + public class AnInnerClass { + private int innerClassId; + + public AnInnerClass(int innerClassId) { + this.innerClassId = innerClassId; + } + + public int getInnerClassId() { + return innerClassId; + } + + public void setInnerClassId(int innerClassId) { + this.innerClassId = innerClassId; + } + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InterfaceWithTypeValue.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InterfaceWithTypeValue.java new file mode 100644 index 00000000000..837a6df7583 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InterfaceWithTypeValue.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; + +/** + * An interface with a different name via the {@Link Type} annotation. + */ +@Type("NewName") +public interface InterfaceWithTypeValue { + int getValue1(); + String getValue2(); +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InvalidNamedTypes.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InvalidNamedTypes.java new file mode 100644 index 00000000000..803f42fa84d --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/InvalidNamedTypes.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Enum; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Input; +import org.eclipse.microprofile.graphql.Interface; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class representing types with invalid names. + */ +@Type("_InvalidName") +public class InvalidNamedTypes { + + @Type("__InvalidName") + public static class InvalidNamedPerson { + private int personId; + private String name; + + public InvalidNamedPerson(int personId, String name) { + this.personId = personId; + this.name = name; + } + + public int getPersonId() { + return personId; + } + + public void setPersonId(int personId) { + this.personId = personId; + } + } + + @Input + @Name("1ThisIsInvalid") + public static class InvalidInputType { + private String string; + + public InvalidInputType(String string) { + this.string = string; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + } + + @Interface("####Name") + public static interface InvalidInterface { + String getName(); + } + + public static class InvalidClass implements InvalidInterface { + + private String name; + + public InvalidClass(String name) { + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + @GraphQLApi + public static class ClassWithInvalidQuery { + @Query("__Name") + public String getString() { + return "string"; + } + } + + @GraphQLApi + public static class ClassWithInvalidMutation { + @Mutation("123BadName") + public String echoString(String string) { + return string; + } + } + + @GraphQLApi + public static class ClassWithInvalidEnum { + + @Query + public Size echoSize(@Name("value") Size size) { + return size; + } + } + + @Enum("&!@@!") + public enum Size { + S, + M, + L, + XL, + XXL, + XXXL + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level0.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level0.java new file mode 100644 index 00000000000..288002f3a44 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level0.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test multiple levels of object graph. + */ +@Type +public class Level0 { + + private String id; + private Level1 level1; + + public Level0(String id, Level1 level1) { + this.id = id; + this.level1 = level1; + } + + public Level0() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Level1 getLevel1() { + return level1; + } + + public void setLevel1(Level1 level1) { + this.level1 = level1; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level1.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level1.java new file mode 100644 index 00000000000..8295808655b --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level1.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.math.BigDecimal; + +/** + * POJO to test multiple levels of object graph. + */ +public class Level1 { + private String code; + private Level2 level2; + private BigDecimal bigDecimal; + + public Level1(String code, Level2 level2, BigDecimal bigDecimal) { + this.code = code; + this.level2 = level2; + this.bigDecimal = bigDecimal; + } + + public Level1() { + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Level2 getLevel2() { + return level2; + } + + public void setLevel2(Level2 level2) { + this.level2 = level2; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public void setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level2.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level2.java new file mode 100644 index 00000000000..bd5125cf139 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Level2.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +/** + * POJO to test multiple levels of object graph. + */ +public class Level2 { + private String code; + private String description; + private Address address; + + public Level2(String code, String description, Address address) { + this.code = code; + this.description = description; + this.address = address; + } + + public Level2() { + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Motorbike.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Motorbike.java new file mode 100644 index 00000000000..1d5c90806c6 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Motorbike.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.Type; + +/** + * Represents a Motorbike. + */ +@Description("A representation of a motorbike") +public class Motorbike extends AbstractVehicle { + + boolean hasSideCar; + + public Motorbike(String plate, int numberOfWheels, String model, String make, int getManufactureYear, boolean hasSideCar) { + super(plate, numberOfWheels, model, make, getManufactureYear); + this.hasSideCar = hasSideCar; + } + + public boolean isHasSideCar() { + return hasSideCar; + } + + public void setHasSideCar(boolean hasSideCar) { + this.hasSideCar = hasSideCar; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/MultiLevelListsAndArrays.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/MultiLevelListsAndArrays.java new file mode 100644 index 00000000000..8dc801f7f1c --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/MultiLevelListsAndArrays.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; + +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test multiple levels of lists and arrays. + */ +@Type +public class MultiLevelListsAndArrays { + + private List> listOfListOfBigDecimal; + private List> listOfListOfPerson; + private List> listOfListOfInteger; + private List listOfStringArrays; + private Collection>> colColColString; + private int[][] intMultiLevelArray; + private Person[][] personMultiLevelArray; + private String[][][] multiStringArray; + + public MultiLevelListsAndArrays(List> listOfListOfBigDecimal, + List> listOfListOfPerson, + List> listOfListOfInteger, + int[][] intMultiLevelArray, + Person[][] personMultiLevelArray, + List listOfStringArrays, + String[][][] multiStringArray, + Collection>> colColColString) { + this.listOfListOfBigDecimal = listOfListOfBigDecimal; + this.listOfListOfPerson = listOfListOfPerson; + this.listOfListOfInteger = listOfListOfInteger; + this.intMultiLevelArray = intMultiLevelArray; + this.personMultiLevelArray = personMultiLevelArray; + this.listOfStringArrays = listOfStringArrays; + this.multiStringArray = multiStringArray; + this.colColColString = colColColString; + } + + public List> getListOfListOfBigDecimal() { + return listOfListOfBigDecimal; + } + + public void setListOfListOfBigDecimal(List> listOfListOfBigDecimal) { + this.listOfListOfBigDecimal = listOfListOfBigDecimal; + } + + public List> getListOfListOfPerson() { + return listOfListOfPerson; + } + + public void setListOfListOfPerson(List> listOfListOfPerson) { + this.listOfListOfPerson = listOfListOfPerson; + } + + public List> getListOfListOfInteger() { + return listOfListOfInteger; + } + + public void setListOfListOfInteger(List> listOfListOfInteger) { + this.listOfListOfInteger = listOfListOfInteger; + } + + public int[][] getIntMultiLevelArray() { + return intMultiLevelArray; + } + + public void setIntMultiLevelArray(int[][] intMultiLevelArray) { + this.intMultiLevelArray = intMultiLevelArray; + } + + public Person[][] getPersonMultiLevelArray() { + return personMultiLevelArray; + } + + public void setPersonMultiLevelArray(Person[][] personMultiLevelArray) { + this.personMultiLevelArray = personMultiLevelArray; + } + + public List getListOfStringArrays() { + return listOfStringArrays; + } + + public void setListOfStringArrays(List listOfStringArrays) { + this.listOfStringArrays = listOfStringArrays; + } + + public String[][][] getMultiStringArray() { + return multiStringArray; + } + + public void setMultiStringArray(String[][][] multiStringArray) { + this.multiStringArray = multiStringArray; + } + + public Collection>> getColColColString() { + return colColColString; + } + + public void setColColColString(Collection>> colColColString) { + this.colColColString = colColColString; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/NullPOJO.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/NullPOJO.java new file mode 100644 index 00000000000..f22374829e9 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/NullPOJO.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.List; + +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Type; + +/** + * POJO to test nulls. + */ +@Type +public class NullPOJO { + + // should be mandatory + private int id; + + // should be optional for type + private Long longValue; + + // should be mandatory + @NonNull + private String stringValue; + + private List<@NonNull String> listNonNullStrings; + private List> listOfListOfNonNullStrings; + + private List> listOfListOfNullStrings; + + // should be nullable for type and non nullable for input type + private String nonNullForInput; + + private String testNullWithGet; + private String testNullWithSet; + + private List testInputOnly; + private List testOutputOnly; + + @NonNull + private List listNullStringsWhichIsMandatory; + + public NullPOJO() { + } + + public NullPOJO(int id, + Long longValue, + String stringValue, + List listNonNullStrings) { + this.id = id; + this.longValue = longValue; + this.stringValue = stringValue; + this.listNonNullStrings = listNonNullStrings; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Long getLongValue() { + return longValue; + } + + public void setLongValue(Long longValue) { + this.longValue = longValue; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public List getListNonNullStrings() { + return listNonNullStrings; + } + + public void setListNonNullStrings(List listNonNullStrings) { + this.listNonNullStrings = listNonNullStrings; + } + + public String getNonNullForInput() { + return nonNullForInput; + } + + // should be non null for input type + @NonNull + public void setNonNullForInput(String nonNullForInput) { + this.nonNullForInput = nonNullForInput; + } + + // should be mandatory for type and optional for input type + @NonNull + public String getTestNullWithGet() { + return testNullWithGet; + } + + public void setTestNullWithGet(String testNullWithGet) { + this.testNullWithGet = testNullWithGet; + } + + public List> getListOfListOfNonNullStrings() { + return listOfListOfNonNullStrings; + } + + public void setListOfListOfNonNullStrings(List> listOfListOfNonNullStrings) { + this.listOfListOfNonNullStrings = listOfListOfNonNullStrings; + } + + public List> getListOfListOfNullStrings() { + return listOfListOfNullStrings; + } + + public void setListOfListOfNullStrings(List> listOfListOfNullStrings) { + this.listOfListOfNullStrings = listOfListOfNullStrings; + } + + public String getTestNullWithSet() { + return testNullWithSet; + } + + // should be mandatory for input type and optional for type + @NonNull + public void setTestNullWithSet(String testNullWithSet) { + this.testNullWithSet = testNullWithSet; + } + + public List getListNullStringsWhichIsMandatory() { + return listNullStringsWhichIsMandatory; + } + + public void setListNullStringsWhichIsMandatory(List listNullStringsWhichIsMandatory) { + this.listNullStringsWhichIsMandatory = listNullStringsWhichIsMandatory; + } + + public List getTestInputOnly() { + return testInputOnly; + } + + // array return type should be mandatory for input only + public void setTestInputOnly(List<@NonNull String> testInputOnly) { + this.testInputOnly = testInputOnly; + } + + // array return type should be mandatory for output only + public List<@NonNull String> getTestOutputOnly() { + return testOutputOnly; + } + + public void setTestOutputOnly(List testOutputOnly) { + this.testOutputOnly = testOutputOnly; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ObjectWithIgnorableFieldsAndMethods.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ObjectWithIgnorableFieldsAndMethods.java new file mode 100644 index 00000000000..1d85669b9d5 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/ObjectWithIgnorableFieldsAndMethods.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import javax.json.bind.annotation.JsonbTransient; + +import org.eclipse.microprofile.graphql.Ignore; +import org.eclipse.microprofile.graphql.Name; + +/** + * Defines an object that has fields that should be ignored. + */ +public class ObjectWithIgnorableFieldsAndMethods { + + private String id; + + @Ignore + private String pleaseIgnore; + + @JsonbTransient + private int ignoreThisAsWell; + + private boolean dontIgnore; + + private int ignoreBecauseOfMethod; + + private String value; + + public ObjectWithIgnorableFieldsAndMethods(String id, String pleaseIgnore, int ignoreThisAsWell, boolean dontIgnore, int ignoreBecauseOfMethod) { + this.id = id; + this.pleaseIgnore = pleaseIgnore; + this.ignoreThisAsWell = ignoreThisAsWell; + this.dontIgnore = dontIgnore; + this.ignoreBecauseOfMethod = ignoreBecauseOfMethod; + } + + public ObjectWithIgnorableFieldsAndMethods() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPleaseIgnore() { + return pleaseIgnore; + } + + public void setPleaseIgnore(String pleaseIgnore) { + this.pleaseIgnore = pleaseIgnore; + } + + public int getIgnoreThisAsWell() { + return ignoreThisAsWell; + } + + public void setIgnoreThisAsWell(int ignoreThisAsWell) { + this.ignoreThisAsWell = ignoreThisAsWell; + } + + public boolean isDontIgnore() { + return dontIgnore; + } + + public void setDontIgnore(boolean dontIgnore) { + this.dontIgnore = dontIgnore; + } + + // should be ignored on output type only + @Ignore + public int getIgnoreBecauseOfMethod() { + return ignoreBecauseOfMethod; + } + + // should be ignored on input type only + @JsonbTransient + public void setIgnoreBecauseOfMethod(int ignoreBecauseOfMethod) { + this.ignoreBecauseOfMethod = ignoreBecauseOfMethod; + } + + public String getValue() { + return value; + } + + @Name("valueSetter") + public void setValue(String value) { + this.value = value; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Person.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Person.java new file mode 100644 index 00000000000..a7ee8f31659 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Person.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.graphql.Type; + +/** + * Class representing a Person to test various getters. + */ +@Type +public class Person { + private int personId; + private String name; + private Address homeAddress; + private Address workAddress; + private BigDecimal creditLimit; + private Collection listQualifications; + private List
previousAddresses; + private int[] intArray; + private String[] stringArray; + private Map addressMap; + private LocalDate localDate; + private long longValue; + private BigDecimal bigDecimal; + + public Person() { + } + + public Person(int personId, + String name, + Address homeAddress, + Address workAddress, + BigDecimal creditLimit, + Collection listQualifications, + List
previousAddresses, + int[] intArray, + String[] stringArray, + Map addressMap, LocalDate localDate, long longValue, BigDecimal bigDecimal) { + this.personId = personId; + this.name = name; + this.homeAddress = homeAddress; + this.workAddress = workAddress; + this.creditLimit = creditLimit; + this.listQualifications = listQualifications; + this.previousAddresses = previousAddresses; + this.intArray = intArray; + this.stringArray = stringArray; + this.addressMap = addressMap; + this.localDate = localDate; + this.longValue = longValue; + this.bigDecimal = bigDecimal; + } + + public long getLongValue() { + return longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + public int getPersonId() { + return personId; + } + + public void setPersonId(int personId) { + this.personId = personId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getHomeAddress() { + return homeAddress; + } + + public void setHomeAddress(Address homeAddress) { + this.homeAddress = homeAddress; + } + + public Address getWorkAddress() { + return workAddress; + } + + public void setWorkAddress(Address workAddress) { + this.workAddress = workAddress; + } + + public BigDecimal getCreditLimit() { + return creditLimit; + } + + public void setCreditLimit(BigDecimal creditLimit) { + this.creditLimit = creditLimit; + } + + public Collection getListQualifications() { + return listQualifications; + } + + public void setListQualifications(Collection listQualifications) { + this.listQualifications = listQualifications; + } + + public List
getPreviousAddresses() { + return previousAddresses; + } + + public void setPreviousAddresses(List
previousAddresses) { + this.previousAddresses = previousAddresses; + } + + public int[] getIntArray() { + return intArray; + } + + public void setIntArray(int[] intArray) { + this.intArray = intArray; + } + + public String[] getStringArray() { + return stringArray; + } + + public void setStringArray(String[] stringArray) { + this.stringArray = stringArray; + } + + public Map getAddressMap() { + return addressMap; + } + + public void setAddressMap(Map addressMap) { + this.addressMap = addressMap; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public void setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + } + + @Override + public String toString() { + return "Person{" + + "personId=" + personId + + ", name='" + name + '\'' + + ", homeAddress=" + homeAddress + + ", workAddress=" + workAddress + + ", creditLimit=" + creditLimit + + ", listQualifications=" + listQualifications + + ", previousAddresses=" + previousAddresses + + ", intArray=" + Arrays.toString(intArray) + + ", stringArray=" + Arrays.toString(stringArray) + + ", addressMap=" + addressMap + + ", longValue=" + longValue + + ", localDate=" + localDate + + ", bigDecimal=" + bigDecimal + + '}'; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithName.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithName.java new file mode 100644 index 00000000000..41b5eee37d9 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithName.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; + +/** + * Class to test Type annotation with value. + */ +@Type("Person") +public class PersonWithName { + private int personId; + + public PersonWithName(int personId) { + this.personId = personId; + } + + public int getPersonId() { + return personId; + } + + public void setPersonId(int personId) { + this.personId = personId; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithNameValue.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithNameValue.java new file mode 100644 index 00000000000..e12c018ebcd --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/PersonWithNameValue.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class to test {@linke Name} annotation with value. + */ +@Type +@Name("Person") +public class PersonWithNameValue { + private int personId; + + public PersonWithNameValue(int personId) { + this.personId = personId; + } + + public int getPersonId() { + return personId; + } + + public void setPersonId(int personId) { + this.personId = personId; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContact.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContact.java new file mode 100644 index 00000000000..899e26172d1 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContact.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; + +import java.util.Objects; + +import javax.json.bind.annotation.JsonbProperty; + +/** + * Defines a simple contact. + */ +public class SimpleContact implements Comparable { + private String id; + private String name; + private int age; + private EnumTestWithEnumName tShirtSize; + + public SimpleContact(String id, String name, int age, EnumTestWithEnumName tShirtSize) { + this.id = id; + this.name = name; + this.age = age; + this.tShirtSize = tShirtSize; + } + + public SimpleContact() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @JsonbProperty("tShirtSize") + public EnumTestWithEnumName getTShirtSize() { + return tShirtSize; + } + + @JsonbProperty("tShirtSize") + public void setTShirtSize(EnumTestWithEnumName tShirtSize) { + this.tShirtSize = tShirtSize; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContact contact = (SimpleContact) o; + return age == contact.age && + Objects.equals(id, contact.id) && + Objects.equals(name, contact.name) && + tShirtSize == contact.tShirtSize; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age, tShirtSize); + } + + @Override + /** + * Simple comparison by age. + */ + public int compareTo(SimpleContact other) { + return Integer.valueOf(age).compareTo(Integer.valueOf(other.getAge())); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputType.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputType.java new file mode 100644 index 00000000000..4619978c980 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputType.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Objects; + +import org.eclipse.microprofile.graphql.Input; + +/** + * Defines a simple contact input type. + */ +@Input +public class SimpleContactInputType { + private String id; + private String name; + private int age; + + public SimpleContactInputType(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public SimpleContactInputType() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContactInputType that = (SimpleContactInputType) o; + return age == that.age + && Objects.equals(id, that.id) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithAddress.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithAddress.java new file mode 100644 index 00000000000..7c0476e8b5a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithAddress.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Input; + +/** + * Defines a simple contact input type which contains an address. + */ +@Input +public class SimpleContactInputTypeWithAddress { + private String id; + private String name; + private int age; + private Address address; + + public SimpleContactInputTypeWithAddress(String id, + String name, + int age, + Address address) { + this.id = id; + this.name = name; + this.age = age; + this.address = address; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithName.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithName.java new file mode 100644 index 00000000000..092d5aa6382 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithName.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Objects; + +import org.eclipse.microprofile.graphql.Input; + +/** + * Defines a simple contact input type with a name. + */ +@Input("MyInputType") +public class SimpleContactInputTypeWithName { + private String id; + private String name; + private int age; + + public SimpleContactInputTypeWithName(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public SimpleContactInputTypeWithName() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContactInputTypeWithName that = (SimpleContactInputTypeWithName) o; + return age == that.age + && Objects.equals(id, that.id) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithNameValue.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithNameValue.java new file mode 100644 index 00000000000..4607762d100 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactInputTypeWithNameValue.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Objects; + +import org.eclipse.microprofile.graphql.Input; +import org.eclipse.microprofile.graphql.Name; + +/** + * Defines a simple contact input type with a {@link Name} value. + */ +@Input +@Name("NameInput") +public class SimpleContactInputTypeWithNameValue { + private String id; + private String name; + private int age; + + public SimpleContactInputTypeWithNameValue(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public SimpleContactInputTypeWithNameValue() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContactInputTypeWithNameValue that = (SimpleContactInputTypeWithNameValue) o; + return age == that.age + && Objects.equals(id, that.id) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithNumberFormats.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithNumberFormats.java new file mode 100644 index 00000000000..f469d9d7cbd --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithNumberFormats.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +import javax.json.bind.annotation.JsonbNumberFormat; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.NumberFormat; +import org.eclipse.microprofile.graphql.Type; + + +/** + * Defines a simple contact which contains a number formats. + */ +@Type +public class SimpleContactWithNumberFormats { + private Integer id; + private String name; + + // this formatting will apply to both Input and standard type + @NumberFormat("0 'years old'") + private Integer age; + + @JsonbNumberFormat(value = "¤ 000.00", locale = "en-AU") + private Float bankBalance; + + @JsonbNumberFormat(value = "000") // this should be ignored + @NumberFormat("0 'value'") // this should be applied + private Integer value; + + private Long longValue; + + @NumberFormat("BigDecimal-##########") + private BigDecimal bigDecimal; + + private List<@DateFormat("DD-MM-YYYY") LocalDate> listDates; + + private List listOfIntegers; + + public Integer getFormatMethod(@NumberFormat("0 'years old'") int age) { + return age; + } + + public SimpleContactWithNumberFormats() { + } + + public SimpleContactWithNumberFormats(Integer id, + String name, Integer age, + Float bankBalance, + int value, + Long longValue, + BigDecimal bigDecimal) { + this.id = id; + this.name = name; + this.age = age; + this.bankBalance = bankBalance; + this.value = value; + this.longValue = longValue; + this.bigDecimal = bigDecimal; + } + + // this format should only apply to the standard type and not Input Type + @NumberFormat("0 'id'") + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Float getBankBalance() { + return bankBalance; + } + + public void setBankBalance(Float bankBalance) { + this.bankBalance = bankBalance; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public void setValue(Integer value) { + this.value = value; + } + + public Long getLongValue() { + return longValue; + } + + public List getListDates() { + return listDates; + } + + public void setListDates(List listDates) { + this.listDates = listDates; + } + + // this format should only be applied to the InputType and not the Type + @NumberFormat("LongValue-##########") + public void setLongValue(Long longValue) { + this.longValue = longValue; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public void setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + } + + public List<@NumberFormat("0 'number'") Integer> getListOfIntegers() { + return listOfIntegers; + } + + public void setListOfIntegers(List<@NumberFormat("0 'number'") Integer> listOfIntegers) { + this.listOfIntegers = listOfIntegers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContactWithNumberFormats contact = (SimpleContactWithNumberFormats) o; + return Objects.equals(id, contact.id) && + Objects.equals(name, contact.name) && + Objects.equals(age, contact.age) && + Objects.equals(bankBalance, contact.bankBalance) && + Objects.equals(value, contact.value) && + Objects.equals(longValue, contact.longValue) && + bigDecimal.compareTo(contact.bigDecimal) == 0 && + Objects.equals(listDates, contact.listDates); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age, bankBalance, value, longValue, bigDecimal, listDates); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithSelf.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithSelf.java new file mode 100644 index 00000000000..f6950e2b237 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleContactWithSelf.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Objects; + +import org.eclipse.microprofile.graphql.Type; + +/** + * Defines a simple contact which also has a reference with self. + */ +@Type +public class SimpleContactWithSelf { + private String id; + private String name; + private int age; + private SimpleContactWithSelf spouse; + + public SimpleContactWithSelf(String id, + String name, + int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public SimpleContactWithSelf getSpouse() { + return spouse; + } + + public void setSpouse(SimpleContactWithSelf spouse) { + this.spouse = spouse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleContactWithSelf that = (SimpleContactWithSelf) o; + return age == that.age + && Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(spouse, that.spouse); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age, spouse); + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTime.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTime.java new file mode 100644 index 00000000000..02ba3a8e64d --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTime.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; +import java.util.List; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class representing a simple date/time type. + */ +@Type +public class SimpleDateTime { + + private List importantDates; + + public SimpleDateTime() { + } + + public SimpleDateTime(List importantDates) { + this.importantDates = importantDates; + } + + @Name("calendarEntries") + public void setImportantDates(List<@DateFormat("dd/MM/yy") LocalDate> importantDates) { + this.importantDates = importantDates; + } + + public List<@DateFormat("dd/MM") LocalDate> getImportantDates() { + return importantDates; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTimePojo.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTimePojo.java new file mode 100644 index 00000000000..fc494cae10a --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/SimpleDateTimePojo.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; +import java.util.List; + +import org.eclipse.microprofile.graphql.DateFormat; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class representing various date/time types. + */ +@Type +public class SimpleDateTimePojo { + + private List formattedListOfDates; + + public SimpleDateTimePojo(List formattedListOfDates) { + this.formattedListOfDates = formattedListOfDates; + } + + public List<@DateFormat("dd/MM") LocalDate> getFormattedListOfDates() { + return formattedListOfDates; + } + + public void setFormattedListOfDates(List formattedListOfDates) { + this.formattedListOfDates = formattedListOfDates; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Task.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Task.java new file mode 100644 index 00000000000..0f5b2de254d --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Task.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; +import java.io.Serializable; +import java.util.UUID; + +/** + * A data class representing a single To Do List task. + */ +@Type +public class Task implements Serializable { + + /** + * The creation time. + */ + private long createdAt; + + /** + * The completion status. + */ + private boolean completed; + + /** + * The task ID. + */ + private String id; + + /** + * The task description. + */ + private String description; + + /** + * Deserialization constructor. + */ + public Task() { + } + + /** + * Construct Task instance. + * + * @param description task description + */ + public Task(String description) { + this.id = UUID.randomUUID().toString().substring(0, 6); + this.createdAt = System.currentTimeMillis(); + this.description = description; + this.completed = false; + } + + /** + * Get the creation time. + * + * @return the creation time + */ + public long getCreatedAt() { + return createdAt; + } + + /** + * Get the task ID. + * + * @return the task ID + */ + public String getId() { + return id; + } + + /** + * Get the task description. + * + * @return the task description + */ + public String getDescription() { + return description; + } + + /** + * Set the task description. + * + * @param description the task description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get the completion status. + * + * @return true if it is completed, false otherwise. + */ + public boolean isCompleted() { + return completed; + } + + /** + * Sets the completion status. + * + * @param completed the completion status + */ + public void setCompleted(boolean completed) { + this.completed = completed; + } + + @Override + public String toString() { + return "Task{" + + "id=" + id + + ", description=" + description + + ", completed=" + completed + + '}'; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIDs.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIDs.java new file mode 100644 index 00000000000..f00322aad63 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIDs.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.UUID; + +import org.eclipse.microprofile.graphql.Id; +import org.eclipse.microprofile.graphql.Type; + +/** + * Class to test multiple ID fields. + */ +@Type +public class TypeWithIDs { + + @Id + private int intId; + + @Id + private Integer integerId; + + @Id + private String stringId; + + @Id + private Long longId; + + @Id + private long longPrimitiveId; + + @Id + private UUID uuidId; + + public TypeWithIDs() { + } + + public TypeWithIDs(int intId, Integer integerId, String stringId, Long longId, long longPrimitiveId, UUID uuidId) { + this.intId = intId; + this.integerId = integerId; + this.stringId = stringId; + this.longId = longId; + this.longPrimitiveId = longPrimitiveId; + this.uuidId = uuidId; + } + + public int getIntId() { + return intId; + } + + public void setIntId(int intId) { + this.intId = intId; + } + + public Integer getIntegerId() { + return integerId; + } + + public void setIntegerId(Integer integerId) { + this.integerId = integerId; + } + + public String getStringId() { + return stringId; + } + + public void setStringId(String stringId) { + this.stringId = stringId; + } + + public Long getLongId() { + return longId; + } + + public void setLongId(Long longId) { + this.longId = longId; + } + + public long getLongPrimitiveId() { + return longPrimitiveId; + } + + public void setLongPrimitiveId(long longPrimitiveId) { + this.longPrimitiveId = longPrimitiveId; + } + + public UUID getUuidId() { + return uuidId; + } + + public void setUuidId(UUID uuidId) { + this.uuidId = uuidId; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnField.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnField.java new file mode 100644 index 00000000000..5c0fae6f5a9 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnField.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Id; + +/** + * Class to test Id annotation. + */ +public class TypeWithIdOnField { + @Id + private int id; + private String name; + + public TypeWithIdOnField(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnMethod.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnMethod.java new file mode 100644 index 00000000000..10b279fd7e3 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithIdOnMethod.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Id; + +/** + * Class to test Id annotation. + */ +public class TypeWithIdOnMethod { + private int id; + private String name; + + public TypeWithIdOnMethod(int id, String name) { + this.id = id; + this.name = name; + } + + @Id + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithMap.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithMap.java new file mode 100644 index 00000000000..5cb644f5e1d --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithMap.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import org.eclipse.microprofile.graphql.Type; +import java.util.Map; + +/** + * POJO to test a {@link Map} as a value. + */ +@Type +public class TypeWithMap { + + private String id; + private Map mapValues; + private Map mapContacts; + + public TypeWithMap(String id, + Map mapValues, + Map mapContacts) { + this.id = id; + this.mapValues = mapValues; + this.mapContacts = mapContacts; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Map getMapValues() { + return mapValues; + } + + public void setMapValues(Map mapValues) { + this.mapValues = mapValues; + } + + public Map getMapContacts() { + return mapContacts; + } + + public void setMapContacts(Map mapContacts) { + this.mapContacts = mapContacts; + } + + @Override + public String toString() { + return "TypeWithMap{" + + "id='" + id + '\'' + + ", mapValues=" + mapValues + + ", mapContacts=" + mapContacts + + '}'; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithNameAndJsonbProperty.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithNameAndJsonbProperty.java new file mode 100644 index 00000000000..5441848c647 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/TypeWithNameAndJsonbProperty.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import javax.json.bind.annotation.JsonbProperty; + +import org.eclipse.microprofile.graphql.Name; + +/** + * Class to test {@link JsonbProperty} annotation on field. + */ +public class TypeWithNameAndJsonbProperty { + + @JsonbProperty("newFieldName1") + private String name1; + + @Name("newFieldName2") + private String name2; + + // @Name annotation should take precedence + @Name("newFieldName3") + @JsonbProperty("thisShouldNotBeUsed") + private String name3; + + private String name4; + private String name5; + private String name6; + + public TypeWithNameAndJsonbProperty(String name1, String name2, String name3, String name4, String name5, String name6) { + this.name1 = name1; + this.name2 = name2; + this.name3 = name3; + this.name4 = name4; + this.name5 = name5; + this.name6 = name6; + } + + public String getName1() { + return name1; + } + + public String getName2() { + return name2; + } + + public String getName3() { + return name3; + } + + @Name("newFieldName4") + public String getName4() { + return name4; + } + + @JsonbProperty("newFieldName5") + public String getName5() { + return name5; + } + + // @Name annotation should take precedence + @Name("newFieldName6") + @JsonbProperty("thisShouldNotBeUsed") + public String getName6() { + return name6; + } +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Vehicle.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Vehicle.java new file mode 100644 index 00000000000..c52af662441 --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/Vehicle.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.util.Collection; + +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.Interface; + +/** + * Interface that represents a vehicle. + */ +@Interface +@Description("Defines common attributes of a vehicle") +public interface Vehicle { + String getPlate(); + int getNumberOfWheels(); + String getMake(); + String getModel(); + int getManufactureYear(); + Collection getIncidents(); +} diff --git a/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/VehicleIncident.java b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/VehicleIncident.java new file mode 100644 index 00000000000..053c95f9c6f --- /dev/null +++ b/microprofile/graphql/server/src/test/java/io/helidon/microprofile/graphql/server/test/types/VehicleIncident.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server.test.types; + +import java.time.LocalDate; + +import org.eclipse.microprofile.graphql.Type; + +/** + * Represents an incident with a {@link Vehicle}. + */ +@Type("Incident") +public class VehicleIncident { + + private LocalDate date; + private String description; + + public VehicleIncident(LocalDate date, String description) { + this.date = date; + this.description = description; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} + diff --git a/microprofile/graphql/server/src/test/resources/META-INF/beans.xml b/microprofile/graphql/server/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e5a9e54aa5e --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/microprofile/graphql/server/src/test/resources/META-INF/microprofile-config.properties b/microprofile/graphql/server/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..cd865c544ee --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mp.initializer.allow=true +mp.initializer.no-warn=true diff --git a/microprofile/graphql/server/src/test/resources/logging.properties b/microprofile/graphql/server/src/test/resources/logging.properties new file mode 100644 index 00000000000..212b2d55558 --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/logging.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +.level=INFO + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n diff --git a/microprofile/graphql/server/src/test/resources/test-results/enum-test-01.txt b/microprofile/graphql/server/src/test/resources/test-results/enum-test-01.txt new file mode 100644 index 00000000000..21256c1824f --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/test-results/enum-test-01.txt @@ -0,0 +1,7 @@ +"T Shirt Size" +enum ShirtSize { + Small + Medium + Large + XLarge +} diff --git a/microprofile/graphql/server/src/test/resources/test-results/enum-test-02.txt b/microprofile/graphql/server/src/test/resources/test-results/enum-test-02.txt new file mode 100644 index 00000000000..a024245cd18 --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/test-results/enum-test-02.txt @@ -0,0 +1,6 @@ +enum ShirtSize { + Small + Medium + Large + XLarge +} diff --git a/microprofile/graphql/server/src/test/resources/test-results/enum-test-03.txt b/microprofile/graphql/server/src/test/resources/test-results/enum-test-03.txt new file mode 100644 index 00000000000..88c992829d7 --- /dev/null +++ b/microprofile/graphql/server/src/test/resources/test-results/enum-test-03.txt @@ -0,0 +1,9 @@ +""" +Description" +""" +enum ShirtSize { + Small + Medium + Large + XLarge +} diff --git a/microprofile/pom.xml b/microprofile/pom.xml index 2812197ce37..3c49527c334 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -53,5 +53,6 @@ reactive-streams messaging cors + graphql diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java index 877d933cf6c..c64c53abc08 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java @@ -35,9 +35,11 @@ */ public class HelidonContainerConfiguration implements ContainerConfiguration { private String appClassName = null; + private String excludeArchivePattern = null; private int port = 8080; private boolean deleteTmp = true; private boolean useRelativePath = false; + private boolean useParentClassloader = true; public String getApp() { return appClassName; @@ -71,6 +73,22 @@ public void setUseRelativePath(boolean b) { this.useRelativePath = b; } + public String getExcludeArchivePattern() { + return excludeArchivePattern; + } + + public void setExcludeArchivePattern(String excludeArchivePattern) { + this.excludeArchivePattern = excludeArchivePattern; + } + + public boolean getUserParentClassloader() { + return useParentClassloader; + } + + public void setUseParentClassloader(boolean useParentClassloader) { + this.useParentClassloader = useParentClassloader; + } + @Override public void validate() throws ConfigurationException { if ((port <= 0) || (port > Short.MAX_VALUE)) { diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java index c8b36daabe0..f4f31b5b9a4 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.FileSystem; @@ -29,22 +30,26 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.spi.CDI; import javax.enterprise.inject.spi.DefinitionException; import io.helidon.config.mp.MpConfigSources; -import io.helidon.microprofile.server.Server; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -86,14 +91,13 @@ public class HelidonDeployableContainer implements DeployableContainer contexts = new HashMap<>(); - private Server dummyServer = null; - @Override public Class getConfigurationClass() { return HelidonContainerConfiguration.class; @@ -102,6 +106,12 @@ public Class getConfigurationClass() { @Override public void setup(HelidonContainerConfiguration configuration) { this.containerConfig = configuration; + String excludeArchivePattern = configuration.getExcludeArchivePattern(); + if (excludeArchivePattern == null || excludeArchivePattern.isBlank()) { + this.excludedLibrariesPattern = null; + } else { + this.excludedLibrariesPattern = Pattern.compile(excludeArchivePattern); + } } @Override @@ -179,7 +189,21 @@ void startServer(RunContext context, Path[] classPath) // there is no server running } - context.classLoader = new MyClassloader(new URLClassLoader(toUrls(classPath))); + URLClassLoader urlClassloader; + ClassLoader parent; + + if (containerConfig.getUserParentClassloader()) { + urlClassloader = new URLClassLoader(toUrls(classPath)); + parent = urlClassloader; + } else { + urlClassloader = new URLClassLoader(toUrls(classPath), null); + parent = Thread.currentThread().getContextClassLoader(); + } + + context.classLoader = new HelidonContainerClassloader(parent, + urlClassloader, + excludedLibrariesPattern, + containerConfig.getUserParentClassloader()); context.oldClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(context.classLoader); @@ -398,7 +422,7 @@ private static class RunContext { */ private Path deployDir; // class loader of this server instance - private MyClassloader classLoader; + private HelidonContainerClassloader classLoader; // class of the runner - loaded once per each run private Class runnerClass; // runner used to run this server instance @@ -407,21 +431,68 @@ private static class RunContext { private ClassLoader oldClassLoader; } - static class MyClassloader extends ClassLoader implements Closeable { + static class HelidonContainerClassloader extends ClassLoader implements Closeable { + private final Pattern excludedLibrariesPattern; private final URLClassLoader wrapped; + private final boolean useParentClassloader; - MyClassloader(URLClassLoader wrapped) { - super(wrapped); + HelidonContainerClassloader(ClassLoader parent, + URLClassLoader wrapped, + Pattern excludedLibrariesPattern, + boolean useParentClassloader) { + super(parent); + + this.excludedLibrariesPattern = excludedLibrariesPattern; this.wrapped = wrapped; + this.useParentClassloader = useParentClassloader; + } + + @Override + public Enumeration getResources(String name) throws IOException { + Set result = new LinkedHashSet<>(); + + Enumeration resources = wrapped.getResources(name); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + result.add(url); + } + + resources = super.getResources(name); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + + if (excludedLibrariesPattern == null) { + result.add(url); + } else { + try { + String path = url.toURI().toString().replace('\\', '/'); + if (!excludedLibrariesPattern.matcher(path).matches()) { + result.add(url); + } + } catch (URISyntaxException e) { + result.add(url); + } + } + } + + return Collections.enumeration(result); } @Override public InputStream getResourceAsStream(String name) { InputStream stream = wrapped.getResourceAsStream(name); - if ((null == stream) && name.startsWith("/")) { + if ((stream == null) && name.startsWith("/")) { stream = wrapped.getResourceAsStream(name.substring(1)); } + if ((stream == null) && useParentClassloader) { + stream = super.getResourceAsStream(name); + } + + if ((stream == null) && useParentClassloader && name.startsWith("/")) { + stream = super.getResourceAsStream(name.substring(1)); + } + return stream; } diff --git a/microprofile/tests/tck/pom.xml b/microprofile/tests/tck/pom.xml index 97b16837aa6..0fc50d0f655 100644 --- a/microprofile/tests/tck/pom.xml +++ b/microprofile/tests/tck/pom.xml @@ -35,6 +35,7 @@ tck-health tck-metrics tck-messaging + tck-graphql tck-jwt-auth tck-openapi tck-opentracing diff --git a/microprofile/tests/tck/tck-graphql/pom.xml b/microprofile/tests/tck/tck-graphql/pom.xml new file mode 100644 index 00000000000..8de5ff7f60c --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/pom.xml @@ -0,0 +1,129 @@ + + + + + + 4.0.0 + + io.helidon.microprofile.tests + tck-project + 2.1.1-SNAPSHOT + + tck-graphql + Helidon Microprofile Tests TCK GraphQL + The microprofile GraphQL TCK + + + + io.helidon.microprofile.tests + helidon-arquillian + ${project.version} + test + + + + org.glassfish.jersey.media + jersey-media-json-binding + + + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + test + + + org.eclipse.microprofile.graphql + microprofile-graphql-tck + test + + + jakarta.xml.bind + jakarta.xml.bind-api + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + generate-sources + + unpack + + + + + org.eclipse.microprofile.graphql + microprofile-graphql-tck + jar + true + ${project.build.directory}/test-classes + + + **/dynamic/,**/*Test.class,**/beans.xml + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + process-classes + + + + ${project.build.directory}/test-classes + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + src/test/tck-suite.xml + + + + + + diff --git a/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/GraphqlExtension.java b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/GraphqlExtension.java new file mode 100644 index 00000000000..00d46243ffa --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/GraphqlExtension.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.tck; + +import org.jboss.arquillian.core.spi.LoadableExtension; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; + +public class GraphqlExtension implements LoadableExtension { + @Override + public void register(ExtensionBuilder extensionBuilder) { + extensionBuilder.service(ResourceProvider.class, UrlResourceProvider.class); + extensionBuilder.service(ResourceProvider.class, UriResourceProvider.class); + } +} diff --git a/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UriResourceProvider.java b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UriResourceProvider.java new file mode 100644 index 00000000000..9c98f1b2353 --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UriResourceProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.tck; + +import java.lang.annotation.Annotation; +import java.net.URI; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; + +/** + * TCKs use addition when creating URL for a client. The default Arquillian implementation returns url without the trailing + * /. + */ +public class UriResourceProvider implements ResourceProvider { + @Override + public Object lookup(ArquillianResource arquillianResource, Annotation... annotations) { + return URI.create("http://localhost:8080/"); + } + + @Override + public boolean canProvide(Class type) { + return URI.class.isAssignableFrom(type); + } +} diff --git a/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UrlResourceProvider.java b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UrlResourceProvider.java new file mode 100644 index 00000000000..17147c2cba7 --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/java/io/helidon/microprofile/graphql/tck/UrlResourceProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.tck; + +import java.lang.annotation.Annotation; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; + +/** + * TCKs use addition when creating URL for a client. The default Arquillian implementation returns url without the trailing + * /. + */ +public class UrlResourceProvider implements ResourceProvider { + @Override + public Object lookup(ArquillianResource arquillianResource, Annotation... annotations) { + try { + return URI.create("http://localhost:8080/").toURL(); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public boolean canProvide(Class type) { + return URL.class.isAssignableFrom(type); + } +} diff --git a/microprofile/tests/tck/tck-graphql/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/microprofile/tests/tck/tck-graphql/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 00000000000..f14b9240b3b --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1,18 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.microprofile.graphql.tck.GraphqlExtension + diff --git a/microprofile/tests/tck/tck-graphql/src/test/resources/arquillian.xml b/microprofile/tests/tck/tck-graphql/src/test/resources/arquillian.xml new file mode 100644 index 00000000000..ae2068a219e --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/resources/arquillian.xml @@ -0,0 +1,36 @@ + + + + + + target/deployments + 8080 + + + + + + true + .*/microprofile-graphql-tck-\d+\.\d+.*?\.jar.* + false + + + \ No newline at end of file diff --git a/microprofile/tests/tck/tck-graphql/src/test/tck-suite.xml b/microprofile/tests/tck/tck-graphql/src/test/tck-suite.xml new file mode 100644 index 00000000000..0273cdb4e0f --- /dev/null +++ b/microprofile/tests/tck/tck-graphql/src/test/tck-suite.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 25b21e20c79..5ae3d8d7571 100644 --- a/pom.xml +++ b/pom.xml @@ -182,6 +182,7 @@ dbclient messaging fault-tolerance + graphql logging diff --git a/tests/integration/mp-graphql/pom.xml b/tests/integration/mp-graphql/pom.xml new file mode 100644 index 00000000000..9cd35cef6ed --- /dev/null +++ b/tests/integration/mp-graphql/pom.xml @@ -0,0 +1,176 @@ + + + + + 4.0.0 + + io.helidon.tests.integration + helidon-tests-integration + 2.1.1-SNAPSHOT + ../pom.xml + + + helidon-tests-integration-mp-graphql + Helidon Integration Test MP GraphQL + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + test + + + io.helidon.microprofile.config + helidon-microprofile-config + test + + + io.helidon.microprofile.server + helidon-microprofile-server + test + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + provided + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.mockito + mockito-core + test + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + test + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + test-jar + tests + ${project.version} + test + + + + org.slf4j + slf4j-jdk14 + test + + + org.glassfish.jersey.core + jersey-client + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + integration-test + + + + ${project.build.directory}/classes + + + ${project.build.directory}/test-classes + + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + listener + org.sonar.java.jacoco.JUnitListener + + + false + + **/*IT.java + + + org.slf4j:slf4j-log4j12 + + + + + verify + verify + + integration-test + verify + + + + + + + diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java new file mode 100644 index 00000000000..14223911444 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.inject.spi.CDI; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import io.helidon.microprofile.cdi.Main; +import io.helidon.microprofile.server.ServerCdiExtension; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.junit.jupiter.api.AfterAll; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Abstract functionality for integration tests via /graphql endpoint. + */ +public abstract class AbstractGraphQLEndpointIT extends AbstractGraphQLTest { + + private static final Logger LOGGER = Logger.getLogger(AbstractGraphQLEndpointIT.class.getName()); + + /** + * Initial GraphiQL query from UI. + */ + protected static final String QUERY_INTROSPECT = "query {\n" + + " __schema {\n" + + " types {\n" + + " name\n" + + " }\n" + + " }\n" + + "}"; + + protected static final String QUERY = "query"; + protected static final String VARIABLES = "variables"; + protected static final String OPERATION = "operationName"; + protected static final String GRAPHQL = "graphql"; + protected static final String UI = "ui"; + + private static String graphQLUrl; + + private static Client client; + + public static Client getClient() { + return client; + } + + /** + * Startup the test and create the Jandex index with the supplied {@link Class}es. + * + * @param clazzes {@link Class}es to add to index + */ + public static void _startupTest(Class... clazzes) throws IOException { + // setup the Jandex index with the required classes + System.clearProperty(JandexUtils.PROP_INDEX_FILE); + String indexFileName = getTempIndexFile(); + setupIndex(indexFileName, clazzes); + System.setProperty(JandexUtils.PROP_INDEX_FILE, indexFileName); + + Main.main(new String[0]); + + ServerCdiExtension current = CDI.current().getBeanManager().getExtension(ServerCdiExtension.class); + + graphQLUrl= "http://127.0.0.1:" + current.port() + "/"; + + System.out.println("GraphQL URL: " + graphQLUrl); + + client = ClientBuilder.newBuilder() + .register(new LoggingFeature(LOGGER, Level.WARNING, LoggingFeature.Verbosity.PAYLOAD_ANY, 32768)) + .property(ClientProperties.FOLLOW_REDIRECTS, true) + .build(); + } + + @AfterAll + public static void teardownTest() { + Main.shutdown(); + } + + /** + * Return a {@link WebTarget} for the graphQL end point. + * + * @return a {@link WebTarget} for the graphQL end point + */ + protected static WebTarget getGraphQLWebTarget() { + Client client = getClient(); + return client.target(graphQLUrl); + } + + /** + * Encode the { and }. + * @param param {@link String} to encode + * @return an encoded @link String} + */ + protected String encode(String param) { + return param == null ? null : param.replaceAll("}", "%7D").replaceAll("\\{", "%7B"); + } + + /** + * Generate a Json Map with a request to send to graphql + * + * @param query the query to send + * @param operation optional operation + * @param variables optional variables + * @return a {@link java.util.Map} + */ + protected Map generateJsonRequest(String query, String operation, Map variables) { + Map map = new HashMap<>(); + map.put(QUERY, query); + map.put(OPERATION, operation); + map.put(VARIABLES, variables); + + return map; + } + + /** + * Return the response as Json. + * + * @param response {@link javax.ws.rs.core.Response} received from web server + * @return the response as Json + */ + protected Map getJsonResponse(Response response) { + String stringResponse = (response.readEntity(String.class)); + assertThat(stringResponse, is(notNullValue())); + return JsonUtils.convertJSONtoMap(stringResponse); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlCdiIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlCdiIT.java new file mode 100644 index 00000000000..3b0019cd8cd --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlCdiIT.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.DisableDiscovery; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +/** + * Common functionality for integration tests. + */ +@HelidonTest +@DisableDiscovery +@AddExtension(GraphQlCdiExtension.class) +abstract class AbstractGraphQlCdiIT extends AbstractGraphQlIT { + + AbstractGraphQlCdiIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension.collectedApis()); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlIT.java new file mode 100644 index 00000000000..3822c8d8e2a --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQlIT.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.graphql.server.InvocationHandler; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.graphql.ConfigKey; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import static io.helidon.graphql.server.GraphQlConstants.DATA; +import static io.helidon.graphql.server.GraphQlConstants.ERRORS; +import static io.helidon.graphql.server.GraphQlConstants.MESSAGE; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +class AbstractGraphQlIT extends AbstractGraphQLTest { + private final Set> classes; + + protected String indexFileName = null; + protected File indexFile = null; + + protected AbstractGraphQlIT(Set> classes) { + this.classes = classes; + } + + @BeforeEach + public void setupTest() throws IOException { + System.clearProperty(JandexUtils.PROP_INDEX_FILE); + indexFileName = getTempIndexFile(); + indexFile = null; + } + + @AfterEach + public void teardownTest() { + if (indexFile != null) { + indexFile.delete(); + } + } + + @SuppressWarnings("unchecked") + protected void assertMessageValue(String query, String expectedMessage, boolean dataExpected) { + InvocationHandler executionContext = createInvocationHandler(); + Map mapResults = executionContext.execute(query); + if (dataExpected && mapResults.size() != 2) { + System.out.println(JsonUtils.convertMapToJson(mapResults)); + } + assertThat(mapResults.size(), is(dataExpected ? 2 : 1)); + List> listErrors = (List>) mapResults.get(ERRORS); + assertThat(listErrors, is(notNullValue())); + assertThat(listErrors.size(), is(1)); + Map mapErrors = listErrors.get(0); + assertThat(mapErrors.get(MESSAGE), is(expectedMessage)); + + assertThat(mapResults.containsKey(DATA), is(dataExpected)); + } + + protected void assertInterfaceResults() { + Schema schema = createSchema(); + assertThat(schema, CoreMatchers.is(notNullValue())); + schema.getTypes().forEach(t -> System.out.println(t.name())); + assertThat(schema.getTypes().size(), CoreMatchers.is(6)); + assertThat(schema.getTypeByName("Vehicle"), CoreMatchers.is(notNullValue())); + assertThat(schema.getTypeByName("Car"), CoreMatchers.is(notNullValue())); + assertThat(schema.getTypeByName("Motorbike"), CoreMatchers.is(notNullValue())); + assertThat(schema.getTypeByName("Incident"), CoreMatchers.is(notNullValue())); + assertThat(schema.getTypeByName("Query"), CoreMatchers.is(notNullValue())); + assertThat(schema.getTypeByName("Mutation"), CoreMatchers.is(notNullValue())); + generateGraphQLSchema(schema); + } + + protected Schema createSchema() { + return SchemaGenerator.builder() + .classes(classes) + .build() + .generateSchema(); + } + + protected InvocationHandler createInvocationHandler() { + InvocationHandler.Builder builder = InvocationHandler.builder(); + Config config = ConfigProvider.getConfig(); + + config.getOptionalValue(ConfigKey.DEFAULT_ERROR_MESSAGE, String.class) + .ifPresent(builder::defaultErrorMessage); + + config.getOptionalValue(ConfigKey.EXCEPTION_WHITE_LIST, String[].class) + .stream() + .flatMap(Arrays::stream) + .forEach(builder::addWhitelistedException); + + config.getOptionalValue(ConfigKey.EXCEPTION_BLACK_LIST, String[].class) + .stream() + .flatMap(Arrays::stream) + .forEach(builder::addBlacklistedException); + + return builder + .schema(createSchema().generateGraphQLSchema()) + .build(); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AllDefaultsExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AllDefaultsExceptionIT.java new file mode 100644 index 00000000000..04c01d459f7 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AllDefaultsExceptionIT.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for exception handling with all defaults. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +class AllDefaultsExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + AllDefaultsExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testAllDefaultsForConfig() throws IOException { + setupIndex(indexFileName); + InvocationHandler executionContext = createInvocationHandler(); + assertThat(executionContext, is(notNullValue())); + assertThat(executionContext.defaultErrorMessage(), is("Server Error")); + assertThat(executionContext.blacklistedExceptions().size(), is(0)); + assertThat(executionContext.whitelistedExceptions().size(), is(0)); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLListAndWLExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLListAndWLExceptionIT.java new file mode 100644 index 00000000000..fd7d7a34fd6 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLListAndWLExceptionIT.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddConfig; + +import org.eclipse.microprofile.graphql.ConfigKey; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for deny list and allow list. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +@AddConfig(key = ConfigKey.EXCEPTION_WHITE_LIST, + value = "org.eclipse.microprofile.graphql.tck.apps.superhero.api.WeaknessNotFoundException") +@AddConfig(key = ConfigKey.EXCEPTION_BLACK_LIST, value = "java.io.IOException,java.util.concurrent.TimeoutException") +class BLListAndWLExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + BLListAndWLExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void tesDenyListAndAllowList() throws IOException { + setupIndex(indexFileName); + + InvocationHandler executionContext = createInvocationHandler(); + assertThat(executionContext.defaultErrorMessage(), is("Server Error")); + assertThat(executionContext.blacklistedExceptions().size(), is(2)); + assertThat(executionContext.whitelistedExceptions().size(), is(1)); + assertThat(executionContext.blacklistedExceptions().contains("java.io.IOException"), is(true)); + assertThat(executionContext.blacklistedExceptions().contains("java.util.concurrent.TimeoutException"), is(true)); + assertThat(executionContext.whitelistedExceptions() + .contains("org.eclipse.microprofile.graphql.tck.apps.superhero.api.WeaknessNotFoundException"), + is(true)); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLOfIOExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLOfIOExceptionIT.java new file mode 100644 index 00000000000..06416d00d70 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/BLOfIOExceptionIT.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddConfig; + +import org.eclipse.microprofile.graphql.ConfigKey; +import org.junit.jupiter.api.Test; + +/** + * Tests for deny list. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +@AddConfig(key = ConfigKey.EXCEPTION_BLACK_LIST, value = "java.io.IOException,java.util.concurrent.TimeoutException") +class BLOfIOExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + BLOfIOExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testDenyListOfIOException() throws IOException { + setupIndex(indexFileName, ExceptionQueries.class); + assertMessageValue("query { checkedException }", "Server Error", true); + assertMessageValue("query { checkedException2 }", "Server Error", true); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DataFetcherUtilsIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DataFetcherUtilsIT.java new file mode 100644 index 00000000000..fef958602c2 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DataFetcherUtilsIT.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesWithArgs; +import io.helidon.microprofile.graphql.server.test.types.ContactRelationship; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithNumberFormats; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static io.helidon.microprofile.graphql.server.JsonUtils.convertObjectToMap; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link DataFetcherUtils} class. + */ +@AddBean(SimpleQueriesWithArgs.class) +@AddBean(TestDB.class) +class DataFetcherUtilsIT extends AbstractGraphQlCdiIT { + + @Inject + DataFetcherUtilsIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + void testSimpleContact() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + Schema schema = createSchema(); + + Map mapContact = Map.of("id", "1", "name", "Tim", "age", 52, "tShirtSize", "L"); + SimpleContact simpleContact = new SimpleContact("1", "Tim", 52, EnumTestWithEnumName.L); + assertArgumentResult(schema, "canFindContact", "contact", mapContact, simpleContact); + } + + @Test + void testSimpleContactWithNumberFormats() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContactWithNumberFormats.class); + Schema schema = createSchema(); + + Map mapContact = Map.of("age", "52 years old", "bankBalance", "$ 100.00", + "bigDecimal", "BigDecimal-123", "longValue", "LongValue-321", + "name", "Tim", "value", "1 value", "id", "100"); + SimpleContactWithNumberFormats contact = + new SimpleContactWithNumberFormats(100, "Tim", 52, 100.0f, 1, 321L, BigDecimal.valueOf(123)); + assertArgumentResult(schema, "canFindSimpleContactWithNumberFormats", "contact", mapContact, contact); + } + + @Test + void testSimpleTypes() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + + Schema schema = createSchema(); + + // ID types + assertArgumentResult(schema, "returnIntegerAsId", "param1", 1, 1); + + UUID uuid = UUID.randomUUID(); + assertArgumentResult(schema, "returnUUIDAsId", "param1", uuid, uuid); + assertArgumentResult(schema, "returnStringAsId", "param1", "abc", "abc"); + assertArgumentResult(schema, "returnStringAsId", "param1", "abc", "abc"); + assertArgumentResult(schema, "returnLongAsId", "param1", 1L, 1L); + assertArgumentResult(schema, "returnLongPrimitiveAsId", "param1", 1L, 1L); + assertArgumentResult(schema, "returnIntPrimitiveAsId", "param1", 2, 2); + + // primitive types + assertArgumentResult(schema, "echoString", "String", "string-value", "string-value"); + assertArgumentResult(schema, "echoInt", "value", 1, 1); + assertArgumentResult(schema, "echoDouble", "value", 100d, 100d); + assertArgumentResult(schema, "echoFloat", "value", 1.1f, 1.1f); + assertArgumentResult(schema, "echoByte", "value", (byte) 10, (byte) 10); + assertArgumentResult(schema, "echoLong", "value", 123L, 123L); + assertArgumentResult(schema, "echoBoolean", "value", true, true); + assertArgumentResult(schema, "echoBoolean", "value", false, false); + assertArgumentResult(schema, "echoChar", "value", 'x', 'x'); + + // Object types + assertArgumentResult(schema, "echoIntegerObject", "value", 1, 1); + assertArgumentResult(schema, "echoDoubleObject", "value", 100d, 100d); + assertArgumentResult(schema, "echoFloatObject", "value", 1.1f, 1.1f); + assertArgumentResult(schema, "echoFloatObject", "value", 1.1f, 1.1f); + assertArgumentResult(schema, "echoByteObject", "value", (byte) 10, (byte) 10); + assertArgumentResult(schema, "echoLongObject", "value", 123L, 123L); + assertArgumentResult(schema, "echoBooleanObject", "value", true, true); + assertArgumentResult(schema, "echoBooleanObject", "value", false, false); + assertArgumentResult(schema, "echoCharacterObject", "value", 'x', 'x'); + + assertArgumentResult(schema, "echoBigDecimal", "value", BigDecimal.valueOf(100.12), BigDecimal.valueOf(100.12)); + assertArgumentResult(schema, "echoBigInteger", "value", BigInteger.valueOf(100), BigInteger.valueOf(100)); + + // Date/Time/DateTime are dealt with in DateTimeIT.java + } + + @Test + void testSimpleTypesWithFormats() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + Schema schema = createSchema(); + + // primitives + assertArgumentResult(schema, "echoIntWithFormat", "value", "100 value", 100); + assertArgumentResult(schema, "echoDoubleWithFormat", "value", "11-format", 11d); + assertArgumentResult(schema, "echoFloatWithFormat", "value", "$ 123.23", 123.23f); + assertArgumentResult(schema, "echoLongWithFormat", "value", "Long-123456", 123456L); + + // objects + assertArgumentResult(schema, "echoIntegerObjectWithFormat", "value", "100 value", 100); + assertArgumentResult(schema, "echoDoubleObjectWithFormat", "value", "11-format", 11d); + assertArgumentResult(schema, "echoFloatObjectWithFormat", "value", "$ 123.23", 123.23f); + assertArgumentResult(schema, "echoLongObjectWithFormat", "value", "Long-123456", 123456L); + + assertArgumentResult(schema, "echoBigDecimalWithFormat", "value", "100-BigDecimal", BigDecimal.valueOf(100.0)); + assertArgumentResult(schema, "echoBigIntegerWithFormat", "value", "100-BigInteger", BigInteger.valueOf(100)); + + // ID + assertArgumentResult(schema, "returnIntegerAsIdWithFormat", "param1", "1 format", 1); + assertArgumentResult(schema, "returnLongAsIdWithFormat", "param1", "1-Long", 1L); + assertArgumentResult(schema, "returnLongPrimitiveAsIdWithFormat", "param1", "2-long", 2L); + assertArgumentResult(schema, "returnIntPrimitiveAsIdWithFormat", "param1", "3 hello", 3); + } + + @Test + void testArrays() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + Schema schema = createSchema(); + + String[] stringArray = new String[] { "A", "B", "C" }; + assertArgumentResult(schema, "echoStringArray", "value", stringArray, stringArray); + + int[] intArray = new int[] { 1, 2, 3, 4 }; + assertArgumentResult(schema, "echoIntArray", "value", intArray, intArray); + + Boolean[] booleanArray = new Boolean[] { true, false, true, true }; + assertArgumentResult(schema, "echoBooleanArray", "value", booleanArray, booleanArray); + + String[][] stringArray2 = new String[][] { + { "A", "B", "C" }, + { "D", "E", "F" } + }; + assertArgumentResult(schema, "echoStringArray2", "value", stringArray2, stringArray2); + + SimpleContact[] contactArray = new SimpleContact[] { + new SimpleContact("c1", "Contact 1", 50, EnumTestWithEnumName.XL), + new SimpleContact("c2", "Contact 2", 52, EnumTestWithEnumName.XL) + }; + assertArgumentResult(schema, "echoSimpleContactArray", "value", contactArray, contactArray); + + // TODO: Test formatting of numbers and dates in arrays + + } + + @Test + void testSimpleCollections() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + Schema schema = createSchema(); + + List listInteger = List.of(1, 2, 3); + List listString = List.of("A", "B", "C"); + Collection colBigInteger = List.of(BigInteger.valueOf(1), BigInteger.valueOf(222), BigInteger.valueOf(333)); + + assertArgumentResult(schema, "echoListOfIntegers", "value", listInteger, listInteger); + assertArgumentResult(schema, "echoListOfStrings", "value", listString, listString); + assertArgumentResult(schema, "echoListOfBigIntegers", "value", colBigInteger, colBigInteger); + + // Test formatting for numbers and dates + List listFormattedIntegers = new ArrayList<>(List.of("1 years old", "3 years old", "53 years old")); + assertArgumentResult(schema, "echoFormattedListOfIntegers", "value", listFormattedIntegers, + List.of(1, 3, 53)); + + LocalDate localDate1 = LocalDate.of(2020, 9, 23); + LocalDate localDate2 = LocalDate.of(2020, 9, 22); + List listLocalDates = List.of("23-09-2020", "22-09-2020"); + assertArgumentResult(schema, "echoFormattedLocalDate", "value", listLocalDates, + List.of(localDate1, localDate2)); + } + + @Test + @SuppressWarnings("unchecked") + void testCollectionsAndObjects() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class); + + Schema schema = createSchema(); + + SimpleContact contact1 = new SimpleContact("c1", "Contact 1", 50, EnumTestWithEnumName.XL); + SimpleContact contact2 = new SimpleContact("c2", "Contact 2", 52, EnumTestWithEnumName.XXL); + + Collection colContacts = new HashSet<>(List.of(contact1, contact2)); + Collection> colOfMaps = new HashSet<>(); + colOfMaps.add(convertObjectToMap(contact1)); + colOfMaps.add(convertObjectToMap(contact2)); + + assertArgumentResult(schema, "echoCollectionOfSimpleContacts", "value", + colOfMaps, colContacts); + + // test multi-level collections + } + + @Test + @SuppressWarnings("unchecked") + void testObjectGraphs() throws Exception { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, SimpleContact.class, ContactRelationship.class); + + Schema schema = createSchema(); + + SimpleContact contact1 = new SimpleContact("c1", "Contact 1", 50, EnumTestWithEnumName.M); + SimpleContact contact2 = new SimpleContact("c2", "Contact 2", 53, EnumTestWithEnumName.L); + ContactRelationship relationship = new ContactRelationship(contact1, contact2, "married"); + + Map contact1Map = convertObjectToMap(contact1); + Map contact2Map = convertObjectToMap(contact2); + + // Create a map representing the above contact relationship + Map mapContactRel = Map.of("relationship", "married", + "contact1", contact1Map, + "contact2", contact2Map); + + assertArgumentResult(schema, "canFindContactRelationship", "relationship", mapContactRel, relationship); + } + + /** + * Validate that the given argument results in the correct value. + * + * @param schema {@link Schema} + * @param fdName the name of the {@link SchemaFieldDefinition} + * @param argumentName the name of the {@link SchemaArgument} + * @param input the input + * @param expected the expected output + * @throws Exception if any errors + */ + @SuppressWarnings( { "rawtypes" }) + protected void assertArgumentResult(Schema schema, String fdName, + String argumentName, Object input, Object expected) throws Exception { + SchemaArgument argument = getArgument(schema, "Query", fdName, argumentName); + assertThat(argument, is(notNullValue())); + Object result = DataFetcherUtils.generateArgumentValue(schema, argument.argumentType(), + argument.originalType(), argument.originalArrayType(), + input, argument.format()); + + if (input instanceof Collection) { + // compare each value + Collection colExpected = (Collection) expected; + Collection colResult = (Collection) result; + for (Object value : colExpected) { + if (!colResult.contains(value)) { + throw new AssertionError("Cannot find expected value [" + + value + "] in result " + colResult.toString()); + } + } + } else { + assertThat(result, is(expected)); + } + } + + protected SchemaArgument getArgument(Schema schema, String typeName, String fdName, String argumentName) { + SchemaType type = schema.getTypeByName(typeName); + if (type != null) { + SchemaFieldDefinition fd = getFieldDefinition(type, fdName); + if (fd != null) { + return fd.arguments().stream() + .filter(a -> a.argumentName().equals(argumentName)) + .findFirst() + .orElse(null); + } + } + return null; + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeIT.java new file mode 100644 index 00000000000..52b6cf7608d --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeIT.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesAndMutations; +import io.helidon.microprofile.graphql.server.test.types.DateTimePojo; +import io.helidon.microprofile.graphql.server.test.types.SimpleDateTime; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static io.helidon.graphql.server.GraphQlConstants.ERRORS; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATETIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_TIME_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.TIME_SCALAR; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for date/times. + */ +@AddBean(SimpleQueriesAndMutations.class) +@AddBean(TestDB.class) +class DateTimeIT extends AbstractGraphQlCdiIT { + + @Inject + DateTimeIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testDifferentSetterGetter() throws IOException { + setupIndex(indexFileName, SimpleDateTime.class, SimpleQueriesAndMutations.class); + InvocationHandler executionContext = createInvocationHandler(); + Map mapResults = getAndAssertResult( + executionContext.execute( + "mutation { echoSimpleDateTime(value: { calendarEntries: [ \"22/09/20\", \"23/09/20\" ] } ) { " + + "importantDates } }")); + assertThat(mapResults, is(notNullValue())); + Map mapResults2 = (Map) mapResults.get("echoSimpleDateTime"); + assertThat(mapResults2.size(), is(1)); + ArrayList listDates = (ArrayList) mapResults2.get("importantDates"); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("22/09")); + assertThat(listDates.get(1), is("23/09")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDateAndTime() throws IOException { + setupIndex(indexFileName, DateTimePojo.class, SimpleQueriesAndMutations.class); + InvocationHandler executionContext = createInvocationHandler(); + + Schema schema = createSchema(); + SchemaType type = schema.getTypeByName("DateTimePojo"); + + SchemaFieldDefinition fd = getFieldDefinition(type, "localDate"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("MM/dd/yyyy")); + assertThat(fd.description(), is(nullValue())); + assertThat(fd.isDefaultFormatApplied(), is(false)); + assertThat(fd.returnType(), is(FORMATTED_DATE_SCALAR)); + + fd = getFieldDefinition(type, "localTime"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("hh:mm[:ss]")); + assertThat(fd.description(), is(nullValue())); + assertThat(fd.isDefaultFormatApplied(), is(false)); + assertThat(fd.returnType(), is(FORMATTED_TIME_SCALAR)); + + fd = getFieldDefinition(type, "localDate2"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("MM/dd/yyyy")); + assertThat(fd.description(), is(nullValue())); + assertThat(fd.isDefaultFormatApplied(), is(false)); + assertThat(fd.returnType(), is(FORMATTED_DATE_SCALAR)); + + // test default values for date and time + assertDefaultFormat(type, "offsetTime", "HH[:mm][:ss]Z", true); + assertDefaultFormat(type, "localTime", "hh:mm[:ss]", false); + assertDefaultFormat(type, "localDateTime", "yyyy-MM-dd'T'HH[:mm][:ss]", true); + assertDefaultFormat(type, "offsetDateTime", "yyyy-MM-dd'T'HH[:mm][:ss]Z", true); + assertDefaultFormat(type, "zonedDateTime", "yyyy-MM-dd'T'HH[:mm][:ss]Z'['VV']'", true); + assertDefaultFormat(type, "localDateNoFormat", "yyyy-MM-dd", true); + assertDefaultFormat(type, "significantDates", "yyyy-MM-dd", true); + assertDefaultFormat(type, "formattedListOfDates", "dd/MM", false); + + // testing the conversion of the following scalars when they have default formatting applied + // FormattedDate -> Date + // FormattedTime -> Time + // FormattedDateTime -> DateTime + + fd = getFieldDefinition(type, "localDateTime"); + assertThat(fd, is(notNullValue())); + assertThat(fd.isDefaultFormatApplied(), is(true)); + assertThat(fd.returnType(), is(DATETIME_SCALAR)); + + fd = getFieldDefinition(type, "localDateNoFormat"); + assertThat(fd, is(notNullValue())); + assertThat(fd.isDefaultFormatApplied(), is(true)); + assertThat(fd.returnType(), is(DATE_SCALAR)); + + fd = getFieldDefinition(type, "localTimeNoFormat"); + assertThat(fd, is(notNullValue())); + assertThat(fd.isDefaultFormatApplied(), is(true)); + assertThat(fd.returnType(), is(TIME_SCALAR)); + + Map mapResults = getAndAssertResult( + executionContext.execute("query { dateAndTimePOJOQuery { offsetDateTime offsetTime zonedDateTime " + + "localDate localDate2 localTime localDateTime significantDates " + + "formattedListOfDates } }")); + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("dateAndTimePOJOQuery"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.size(), is(9)); + + assertThat(mapResults2.get("localDate"), is("02/17/1968")); + assertThat(mapResults2.get("localDate2"), is("08/04/1970")); + assertThat(mapResults2.get("localTime"), is("10:10:20")); + assertThat(mapResults2.get("offsetTime"), is("08:10:01+0000")); + Object significantDates = mapResults2.get("significantDates"); + assertThat(significantDates, is(notNullValue())); + List listDates = (ArrayList) mapResults2.get("significantDates"); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("1968-02-17")); + assertThat(listDates.get(1), is("1970-08-04")); + + listDates = (List) mapResults2.get("formattedListOfDates"); + assertThat(listDates, is(notNullValue())); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("17/02")); + assertThat(listDates.get(1), is("04/08")); + + mapResults = getAndAssertResult( + executionContext.execute("query { localDateListFormat }")); + assertThat(mapResults, is(notNullValue())); + listDates = (ArrayList) mapResults.get("localDateListFormat"); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("17/02/1968")); + assertThat(listDates.get(1), is("04/08/1970")); + + // test formats on queries + SchemaType typeQuery = schema.getTypeByName("Query"); + fd = getFieldDefinition(typeQuery, "localDateNoFormat"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("yyyy-MM-dd")); + assertThat(fd.description(), is(nullValue())); + assertThat(fd.isDefaultFormatApplied(), is(true)); + assertThat(fd.returnType(), is(DATE_SCALAR)); + + mapResults = getAndAssertResult( + executionContext + .execute("query { echoFormattedLocalDateWithReturnFormat(value: [ \"23-09-2020\", \"22-09-2020\" ]) }")); + assertThat(mapResults, is(notNullValue())); + listDates = (ArrayList) mapResults.get("echoFormattedLocalDateWithReturnFormat"); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("23/09")); + assertThat(listDates.get(1), is("22/09")); + + SchemaType typeMutation = schema.getTypeByName("Mutation"); + fd = getFieldDefinition(typeMutation, "echoFormattedDateWithJsonB"); + assertThat(fd, is(notNullValue())); + Optional argument = fd.arguments() + .stream().filter(a -> a.argumentName().equals("dates")).findFirst(); + assertThat(argument.isPresent(), is(true)); + SchemaArgument a = argument.get(); + assertThat(a.format()[0], is("MM/dd/yyyy")); + + mapResults = getAndAssertResult( + executionContext.execute("mutation { echoFormattedDateWithJsonB(dates: [ \"09/22/2020\", \"09/23/2020\" ]) }")); + assertThat(mapResults, is(notNullValue())); + listDates = (ArrayList) mapResults.get("echoFormattedDateWithJsonB"); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("22/09/2020")); + assertThat(listDates.get(1), is("23/09/2020")); + + mapResults = getAndAssertResult(executionContext.execute( + "query { echoOffsetDateTime(value: \"29 Jan 2020 at 09:45 in zone +0200\") }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoOffsetDateTime"), is("2020-01-29T09:45:00+0200")); + + mapResults = getAndAssertResult(executionContext.execute( + "query { echoZonedDateTime(value: \"19 February 1900 at 12:00 in Africa/Johannesburg\") }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoZonedDateTime"), is("1900-02-19T12:00:00+0130[Africa/Johannesburg]")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDatesAndMutations() throws IOException { + setupIndex(indexFileName, DateTimePojo.class, SimpleQueriesAndMutations.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("mutation { dateTimePojoMutation { formattedListOfDates localDateTime } }")); + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("dateTimePojoMutation"); + assertThat(mapResults2, is(notNullValue())); + + List listDates = (List) mapResults2.get("formattedListOfDates"); + assertThat(listDates, is(notNullValue())); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("17/02")); + assertThat(listDates.get(1), is("04/08")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoLocalDate(dateArgument: \"17/02/1968\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoLocalDate"), is("1968-02-17")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoLocalDateAU(dateArgument: \"17/02/1968\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoLocalDateAU"), is("17 Feb. 1968")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoLocalDateGB(dateArgument: \"17/02/1968\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoLocalDateGB"), is("17 Feb 1968")); + + mapResults = getAndAssertResult(executionContext.execute("query { queryLocalDateGB }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("queryLocalDateGB"), is("17 Feb 1968")); + + mapResults = getAndAssertResult(executionContext.execute("query { queryLocalDateAU }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("queryLocalDateAU"), is("17 Feb. 1968")); + + Map results = executionContext.execute("mutation { echoLocalDate(dateArgument: \"Today\") }"); + List> listErrors = (List>) results.get(ERRORS); + assertThat(listErrors, is(notNullValue())); + assertThat(listErrors.size(), is(1)); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoLocalTime(time: \"15:13:00\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoLocalTime"), is("15:13")); + + mapResults = getAndAssertResult( + executionContext.execute("mutation { testDefaultFormatLocalDateTime(dateTime: \"2020-01-12T10:00:00\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("testDefaultFormatLocalDateTime"), is("10:00:00 12-01-2020")); + + mapResults = getAndAssertResult( + executionContext.execute("query { transformedDate }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("transformedDate"), is("16 Aug. 2016")); + } + + @Test + public void testDateInputsAsPojo() throws IOException { + setupIndex(indexFileName, DateTimePojo.class, SimpleQueriesAndMutations.class); + InvocationHandler executionContext = createInvocationHandler(); + + validateResult(executionContext, "query { echoDateTimePojo ( " + + " value: { localDate: \"02/17/1968\" " + + "}) { localDate } }", + "localDate", "02/17/1968"); + + validateResult(executionContext, "query { echoDateTimePojo ( " + + " value: { localDate2: \"02/17/1968\" " + + "}) { localDate2 } }", + "localDate2", "02/17/1968"); + + validateResult(executionContext, "query { echoDateTimePojo ( " + + " value: { offsetDateTime: \"1968-02-17T10:12:23+0200\" " + + "}) { offsetDateTime } }", + "offsetDateTime", "1968-02-17T10:12:23+0200"); + + validateResult(executionContext, "query { echoDateTimePojo ( " + + " value: { zonedDateTime: \"1968-02-17T10:12:23+0200[Africa/Johannesburg]\" " + + "}) { zonedDateTime } }", + "zonedDateTime", "1968-02-17T10:12:23+0200[Africa/Johannesburg]"); + + + List listResults = List.of("1968-02-17", "1968-02-18"); + validateResult(executionContext, "query { echoDateTimePojo ( " + + " value: { significantDates: [\"1968-02-17\", \"1968-02-18\" ]" + + "}) { significantDates } }", + "significantDates", List.of("1968-02-17", "1968-02-18")); + + // TODO: Fix +// validateResult(executionContext, "query { echoDateTimePojo ( " +// + " value: { localTime: \"10:22:00\" " +// + "}) { localTime } }", +// "localTime", "10:22:00"); + + } + + @SuppressWarnings("unchecked") + private void validateResult(InvocationHandler executionContext, String query, String field, Object expectedResult) { + Map mapResults = getAndAssertResult( + executionContext.execute(query)); + assertThat(mapResults, is(notNullValue())); + Map mapResults2 = (Map) mapResults.get("echoDateTimePojo"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get(field), is(expectedResult)); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeScalarIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeScalarIT.java new file mode 100644 index 00000000000..2f61bb735c9 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DateTimeScalarIT.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.DateTimeScalarQueries; +import io.helidon.microprofile.graphql.server.test.types.SimpleDateTimePojo; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for date/time scalars. + */ +@AddBean(DateTimeScalarQueries.class) +@AddBean(TestDB.class) +class DateTimeScalarIT extends AbstractGraphQlCdiIT { + + @Inject + DateTimeScalarIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testDateAndTime() throws IOException { + setupIndex(indexFileName, SimpleDateTimePojo.class, DateTimeScalarQueries.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("query { echoSimpleDateTimePojo (dates:[\"2020-01-13\"," + + "\"2021-02-14\"]) { formattedListOfDates } }")); + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("echoSimpleDateTimePojo"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.size(), is(1)); + + List listDates = (ArrayList) mapResults2.get("formattedListOfDates"); + assertThat(listDates, is(notNullValue())); + assertThat(listDates.size(), is(2)); + assertThat(listDates.get(0), is("13/01")); + assertThat(listDates.get(1), is("14/02")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoLocalTime(time: \"15:13:00\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoLocalTime"), is("15:13")); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultCheckedExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultCheckedExceptionIT.java new file mode 100644 index 00000000000..4a5218e34f4 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultCheckedExceptionIT.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +/** + * Tests for default exceptions. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +class DefaultCheckedExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + DefaultCheckedExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + void testBlackListAndWhiteList() throws IOException { + setupIndex(indexFileName, ExceptionQueries.class); + assertMessageValue("query { checkedQuery1(throwException: true) }", "exception", true); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultValuesIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultValuesIT.java new file mode 100644 index 00000000000..effdf2705eb --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DefaultValuesIT.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.DefaultValueQueries; +import io.helidon.microprofile.graphql.server.test.queries.OddNamedQueriesAndMutations; +import io.helidon.microprofile.graphql.server.test.types.DefaultValuePOJO; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for default values. + */ +@AddBean(DefaultValueQueries.class) +@AddBean(OddNamedQueriesAndMutations.class) +@AddBean(TestDB.class) +class DefaultValuesIT extends AbstractGraphQlCdiIT { + @Inject + DefaultValuesIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void setOddNamedQueriesAndMutations() throws IOException { + setupIndex(indexFileName, DefaultValuePOJO.class, OddNamedQueriesAndMutations.class); + + Schema schema = createSchema(); + + assertThat(schema, is(notNullValue())); + SchemaType query = schema.getTypeByName("Query"); + SchemaType mutation = schema.getTypeByName("Mutation"); + assertThat(query, is(notNullValue())); + assertThat(query.fieldDefinitions().stream().filter(fd -> fd.name().equals("settlement")).count(), is(1L)); + assertThat(mutation.fieldDefinitions().stream().filter(fd -> fd.name().equals("getaway")).count(), is(1L)); + } + + @Test + @SuppressWarnings("unchecked") + public void testDefaultValues() throws IOException { + setupIndex(indexFileName, DefaultValuePOJO.class, DefaultValueQueries.class); + InvocationHandler executionContext = createInvocationHandler(); + + // test with both fields as default + Map mapResults = getAndAssertResult( + executionContext.execute("mutation { generateDefaultValuePOJO { id value } }")); + assertThat(mapResults.size(), is(1)); + Map results = (Map) mapResults.get("generateDefaultValuePOJO"); + assertThat(results, is(notNullValue())); + assertThat(results.get("id"), is("ID-1")); + assertThat(results.get("value"), is(1000)); + + // test with a field overridden + mapResults = getAndAssertResult( + executionContext.execute("mutation { generateDefaultValuePOJO(id: \"ID-123\") { id value } }")); + assertThat(mapResults.size(), is(1)); + results = (Map) mapResults.get("generateDefaultValuePOJO"); + assertThat(results, is(notNullValue())); + assertThat(results.get("id"), is("ID-123")); + assertThat(results.get("value"), is(1000)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoDefaultValuePOJO { id value } }")); + assertThat(mapResults.size(), is(1)); + results = (Map) mapResults.get("echoDefaultValuePOJO"); + assertThat(results, is(notNullValue())); + assertThat(results.get("id"), is("ID-1")); + assertThat(results.get("value"), is(1000)); + + // check that the generated default value has the fields in correct order + Schema schema = createSchema(); + SchemaType query = schema.getTypeByName(Schema.QUERY); + assertThat(query, is(notNullValue())); + SchemaFieldDefinition fd = query.getFieldDefinitionByName("echoDefaultValuePOJO"); + assertThat(fd, is(notNullValue())); + SchemaArgument argument = fd.arguments().get(0); + assertThat(argument, is(notNullValue())); + assertThat(argument.defaultValue(), is( + "{ \"id\": \"ID-1\", \"value\": 1000, \"booleanValue\": true, \"dateObject\": \"1968-02-17\"," + + " \"formattedIntWithDefault\": \"2 value\", \"offsetDateTime\": \"29 Jan 2020 at 09:45 in zone " + + "+0200\"}")); + + mapResults = getAndAssertResult( + executionContext.execute("query { echoDefaultValuePOJO(input: {id: \"X123\" value: 1}) { id value } }")); + assertThat(mapResults.size(), is(1)); + results = (Map) mapResults.get("echoDefaultValuePOJO"); + assertThat(results, is(notNullValue())); + assertThat(results.get("id"), is("X123")); + assertThat(results.get("value"), is(1)); + + mapResults = getAndAssertResult( + executionContext.execute("query { echoDefaultValuePOJO(input: {value: 1}) { id value } }")); + assertThat(mapResults.size(), is(1)); + results = (Map) mapResults.get("echoDefaultValuePOJO"); + assertThat(results, is(notNullValue())); + assertThat(results.get("id"), is("ID-123")); + assertThat(results.get("value"), is(1)); + + schema = createSchema(); + SchemaType type = schema.getInputTypeByName("DefaultValuePOJOInput"); + assertReturnTypeDefaultValue(type, "id", "ID-123"); + assertReturnTypeDefaultValue(type, "booleanValue", "false"); + assertReturnTypeMandatory(type, "booleanValue", false); + + fd = getFieldDefinition(type, "value"); + assertThat(fd, is(notNullValue())); + assertThat(fd.defaultValue(), is("111222")); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DescriptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DescriptionIT.java new file mode 100644 index 00000000000..c8014d03ddf --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DescriptionIT.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.DescriptionQueries; +import io.helidon.microprofile.graphql.server.test.types.DescriptionType; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for descriptions. + */ +@AddBean(DescriptionQueries.class) +@AddBean(TestDB.class) +class DescriptionIT extends AbstractGraphQlCdiIT { + + @Inject + DescriptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testDescriptions() throws IOException { + setupIndex(indexFileName, DescriptionType.class, DescriptionQueries.class); + Schema schema = createSchema(); + + assertThat(schema, is(notNullValue())); + SchemaType type = schema.getTypeByName("DescriptionType"); + assertThat(type, is(notNullValue())); + type.fieldDefinitions().forEach(fd -> { + if (fd.name().equals("id")) { + assertThat(fd.description(), is("this is the description")); + } + if (fd.name().equals("value")) { + assertThat(fd.description(), is("description of value")); + } + if (fd.name().equals("longValue1")) { + // no description so include the format + assertThat(fd.description(), is(nullValue())); + assertThat(fd.format()[0], is("L-########")); + } + if (fd.name().equals("longValue2")) { + // both description and formatting + assertThat(fd.description(), is("Description")); + } + }); + + SchemaInputType inputType = schema.getInputTypeByName("DescriptionTypeInput"); + assertThat(inputType, is(notNullValue())); + inputType.fieldDefinitions().forEach(fd -> { + if (fd.name().equals("value")) { + assertThat(fd.description(), is("description on set for input")); + } + }); + + SchemaType query = schema.getTypeByName("Query"); + assertThat(query, is(notNullValue())); + SchemaFieldDefinition fd = getFieldDefinition(query, "descriptionOnParam"); + assertThat(fd, (is(notNullValue()))); + + fd.arguments().forEach(a -> { + if (a.argumentName().equals("param1")) { + assertThat(a.description(), is("Description for param1")); + } + }); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DifferentMessageExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DifferentMessageExceptionIT.java new file mode 100644 index 00000000000..3eebc4c4d60 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DifferentMessageExceptionIT.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddConfig; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for different exception messages. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +@AddConfig(key = "mp.graphql.defaultErrorMessage", value = "new message") +class DifferentMessageExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + DifferentMessageExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testDifferentMessage() throws IOException { + setupIndex(indexFileName); + + InvocationHandler executionContext = createInvocationHandler(); + assertThat(executionContext.defaultErrorMessage(), is("new message")); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DuplicateQueriesAndMutationsIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DuplicateQueriesAndMutationsIT.java new file mode 100644 index 00000000000..9f1bb43b441 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/DuplicateQueriesAndMutationsIT.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.mutations.VoidMutations; +import io.helidon.microprofile.graphql.server.test.queries.DuplicateNameQueries; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for duplicate queries and mutations. + */ +class DuplicateQueriesAndMutationsIT extends AbstractGraphQlIT { + public DuplicateQueriesAndMutationsIT() { + super(Set.of(VoidMutations.class)); + } + + @Test + public void testDuplicateQueryOrMutationNames() throws IOException { + setupIndex(indexFileName, DuplicateNameQueries.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java new file mode 100644 index 00000000000..8d9f1c6883c --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.helidon.microprofile.config.ConfigCdiExtension; +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.DisableDiscovery; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.graphql.server.GraphQlConstants.GRAPHQL_SCHEMA_URI; +import static io.helidon.graphql.server.GraphQlConstants.GRAPHQL_WEB_CONTEXT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for microprofile-graphql implementation via /graphql endpoint. + */ +@HelidonTest +@DisableDiscovery +@AddExtension(GraphQlCdiExtension.class) +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(ConfigCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(NoopQueriesAndMutations.class) +public class GraphQLEndpointIT + extends AbstractGraphQLEndpointIT { + + @BeforeAll + public static void setup() throws IOException { + _startupTest(Person.class); + } + + @Test + public void basicEndpointTests() { + // test /graphql endpoint + WebTarget webTarget = getGraphQLWebTarget().path(GRAPHQL); + Map mapRequest = generateJsonRequest(QUERY_INTROSPECT, null, null); + + // test POST + Response response = webTarget.request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.json(JsonUtils.convertMapToJson(mapRequest))); + assertThat(response, is(notNullValue())); + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + + Map graphQLResults = getJsonResponse(response); + System.err.println(JsonUtils.convertMapToJson(graphQLResults)); + assertThat(graphQLResults.size(), CoreMatchers.is(1)); + + // test GET + webTarget = getGraphQLWebTarget().path(GRAPHQL) + .queryParam(QUERY, encode((String) mapRequest.get(QUERY))) + .queryParam(OPERATION, encode((String) mapRequest.get(OPERATION))) + .queryParam(VARIABLES, encode((String) mapRequest.get(VARIABLES))); + + response = webTarget.request(MediaType.APPLICATION_JSON_TYPE).get(); + assertThat(response, is(notNullValue())); + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + graphQLResults = getJsonResponse(response); + System.err.println(JsonUtils.convertMapToJson(graphQLResults)); + assertThat(graphQLResults.size(), CoreMatchers.is(1)); + } + + @Test + public void testGetSchema() { + WebTarget webTarget = getGraphQLWebTarget().path(GRAPHQL_WEB_CONTEXT).path(GRAPHQL_SCHEMA_URI); + Response response = webTarget.request(MediaType.TEXT_PLAIN).get(); + assertThat(response, is(notNullValue())); + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java new file mode 100644 index 00000000000..ee8ce18feb5 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.QueriesWithIgnorable; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for ignorable fields. + */ +@AddBean(QueriesWithIgnorable.class) +@AddBean(TestDB.class) +class IngorableIT extends AbstractGraphQlCdiIT { + + @Inject + IngorableIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testIgnorable() throws IOException { + setupIndex(indexFileName, QueriesWithIgnorable.class); + InvocationHandler executionContext = createInvocationHandler(); + Map mapResults = getAndAssertResult( + executionContext.execute("query { testIgnorableFields { id dontIgnore } }")); + assertThat(mapResults.size(), is(1)); + + Map mapResults2 = (Map) mapResults.get("testIgnorableFields"); + assertThat(mapResults2.size(), is(2)); + assertThat(mapResults2.get("id"), is("id")); + assertThat(mapResults2.get("dontIgnore"), is(true)); + + // ensure getting the fields generates an error that is caught by the getAndAssertResult + assertThrows(AssertionFailedError.class, () -> getAndAssertResult(executionContext + .execute( + "query { testIgnorableFields { id " + + "dontIgnore pleaseIgnore " + + "ignoreThisAsWell } }"))); + + Schema schema = createSchema(); + SchemaType type = schema.getTypeByName("ObjectWithIgnorableFieldsAndMethods"); + assertThat(type, is(notNullValue())); + assertThat(type.fieldDefinitions().stream().filter(fd -> fd.name().equals("ignoreGetMethod")).count(), is(0L)); + + SchemaInputType inputType = schema.getInputTypeByName("ObjectWithIgnorableFieldsAndMethodsInput"); + assertThat(inputType, is(notNullValue())); + assertThat(inputType.fieldDefinitions().stream().filter(fd -> fd.name().equals("ignoreBecauseOfMethod")).count(), + is(0L)); + assertThat(inputType.fieldDefinitions().stream().filter(fd -> fd.name().equals("valueSetter")).count(), is(1L)); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InputTypeIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InputTypeIT.java new file mode 100644 index 00000000000..ab467da1edb --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InputTypeIT.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.types.SimpleContactInputType; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactInputTypeWithAddress; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactInputTypeWithName; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactInputTypeWithNameValue; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +/** + * Tests for input types. + */ +@AddBean(SimpleContactInputType.class) +@AddBean(SimpleContactInputTypeWithName.class) +@AddBean(SimpleContactInputTypeWithNameValue.class) +@AddBean(SimpleContactInputTypeWithAddress.class) +@AddBean(NoopQueriesAndMutations.class) +class InputTypeIT extends AbstractGraphQlCdiIT { + + @Inject + InputTypeIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + void testInputType() throws IOException { + setupIndex(indexFileName, SimpleContactInputType.class, SimpleContactInputTypeWithName.class, + SimpleContactInputTypeWithNameValue.class, SimpleContactInputTypeWithAddress.class, + NoopQueriesAndMutations.class); + Schema schema = createSchema(); + + assertAll( + () -> assertThat(schema.getInputTypes().size(), is(5)), + () -> assertThat("MyInputType", schema.containsInputTypeWithName("MyInputType"), is(true)), + () -> assertThat("SimpleContactInputTypeInput", + schema.containsInputTypeWithName("SimpleContactInputTypeInput"), + is(true)), + () -> assertThat("NameInput", schema.containsInputTypeWithName("NameInput"), is(true)), + () -> assertThat("SimpleContactInputTypeWithAddressInput", + schema.containsInputTypeWithName("SimpleContactInputTypeWithAddressInput"), + is(true)), + () -> assertThat("AddressInput", schema.containsInputTypeWithName("AddressInput"), is(true)) + ); + + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceOnlyAnnotatedIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceOnlyAnnotatedIT.java new file mode 100644 index 00000000000..7c54899ba83 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceOnlyAnnotatedIT.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.AbstractVehicle; +import io.helidon.microprofile.graphql.server.test.types.Car; +import io.helidon.microprofile.graphql.server.test.types.Motorbike; +import io.helidon.microprofile.graphql.server.test.types.Vehicle; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +/** + * Tests for discovery of interfaces when only the interface annotated. + */ +@AddBean(Vehicle.class) +@AddBean(Car.class) +@AddBean(Motorbike.class) +@AddBean(AbstractVehicle.class) +@AddBean(TestDB.class) +@AddBean(NoopQueriesAndMutations.class) +class InterfaceOnlyAnnotatedIT extends AbstractGraphQlCdiIT { + + @Inject + InterfaceOnlyAnnotatedIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + void testInterfaceDiscoveryWithImplementorsWithNoTypeAnnotation() throws IOException { + setupIndex(indexFileName, Vehicle.class, Car.class, Motorbike.class, AbstractVehicle.class, + NoopQueriesAndMutations.class); + assertInterfaceResults(); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceTypeOnlyAnnotatedIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceTypeOnlyAnnotatedIT.java new file mode 100644 index 00000000000..e9b1eeccd1c --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InterfaceTypeOnlyAnnotatedIT.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.io.IOException; + +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.types.AbstractVehicle; +import io.helidon.microprofile.graphql.server.test.types.Car; +import io.helidon.microprofile.graphql.server.test.types.Motorbike; +import io.helidon.microprofile.graphql.server.test.types.Vehicle; +import io.helidon.microprofile.graphql.server.test.types.VehicleIncident; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +/** + * Tests for interfaces and subsequent unresolved type which has a Name annotation . + */ +@AddBean(Vehicle.class) +@AddBean(Car.class) +@AddBean(Motorbike.class) +@AddBean(VehicleIncident.class) +@AddBean(TestDB.class) +@AddBean(NoopQueriesAndMutations.class) +public class InterfaceTypeOnlyAnnotatedIT extends AbstractGraphQlCdiIT { + + @Inject + InterfaceTypeOnlyAnnotatedIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testInterfaceDiscoveryWithUnresolvedType() throws IOException, IntrospectionException, ClassNotFoundException { + setupIndex(indexFileName, Vehicle.class, Car.class, Motorbike.class, VehicleIncident.class, + AbstractVehicle.class, NoopQueriesAndMutations.class); + assertInterfaceResults(); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedEnumIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedEnumIT.java new file mode 100644 index 00000000000..762f6e850bf --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedEnumIT.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid named enums. + */ +class InvalidNamedEnumIT extends AbstractGraphQlIT { + + protected InvalidNamedEnumIT() { + super(Set.of(InvalidNamedTypes.Size.class, InvalidNamedTypes.ClassWithInvalidEnum.class)); + } + + @Test + public void testInvalidNameEnum() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.Size.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInputTypeIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInputTypeIT.java new file mode 100644 index 00000000000..1f72df0f9de --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInputTypeIT.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid named input types. + */ +class InvalidNamedInputTypeIT extends AbstractGraphQlIT { + @Inject + public InvalidNamedInputTypeIT() { + super(Set.of(InvalidNamedTypes.InvalidNamedPerson.class)); + } + + @Test + void testInvalidNamedInputType() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.InvalidInputType.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInterfaceIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInterfaceIT.java new file mode 100644 index 00000000000..1b1991c5bc7 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedInterfaceIT.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid names interface. + */ +class InvalidNamedInterfaceIT extends AbstractGraphQlIT { + InvalidNamedInterfaceIT() { + super(Set.of(InvalidNamedTypes.InvalidNamedPerson.class)); + } + + @Test + public void testInvalidNamedInterface() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.InvalidInterface.class, InvalidNamedTypes.InvalidClass.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedMutationIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedMutationIT.java new file mode 100644 index 00000000000..3d4de8f78c3 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedMutationIT.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for inalid name mutations. + */ +class InvalidNamedMutationIT extends AbstractGraphQlIT { + + InvalidNamedMutationIT() { + super(Set.of(InvalidNamedTypes.ClassWithInvalidMutation.class)); + } + + @Test + void testInvalidNamedMutation() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.ClassWithInvalidMutation.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedQueryIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedQueryIT.java new file mode 100644 index 00000000000..c7f4ba153dd --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedQueryIT.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid named queries. + */ +public class InvalidNamedQueryIT extends AbstractGraphQlIT { + InvalidNamedQueryIT() { + super(Set.of(InvalidNamedTypes.ClassWithInvalidQuery.class)); + } + + @Test + public void testInvalidNamedQuery() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.ClassWithInvalidQuery.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedTypeIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedTypeIT.java new file mode 100644 index 00000000000..9875b994439 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidNamedTypeIT.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.types.InvalidNamedTypes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid named types. + */ +class InvalidNamedTypeIT extends AbstractGraphQlIT { + InvalidNamedTypeIT() { + super(Set.of(InvalidNamedTypes.InvalidNamedPerson.class)); + } + @Test + public void testInvalidNamedType() throws IOException { + setupIndex(indexFileName, InvalidNamedTypes.InvalidNamedPerson.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidQueriesIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidQueriesIT.java new file mode 100644 index 00000000000..3b4167c9639 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/InvalidQueriesIT.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.mutations.VoidMutations; +import io.helidon.microprofile.graphql.server.test.queries.InvalidQueries; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for invalid queries. + */ +class InvalidQueriesIT extends AbstractGraphQlIT { + + InvalidQueriesIT() { + super(Set.of(VoidMutations.class)); + } + + @Test + void testInvalidQueries() throws IOException { + setupIndex(indexFileName, InvalidQueries.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/Level0IT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/Level0IT.java new file mode 100644 index 00000000000..028a9caf54b --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/Level0IT.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.io.IOException; + +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.types.Level0; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for multi-level object graphs - Level0. + */ +@AddBean(Level0.class) +@AddBean(NoopQueriesAndMutations.class) +class Level0IT extends AbstractGraphQlCdiIT { + + @Inject + Level0IT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testLevel0() throws IOException, IntrospectionException, ClassNotFoundException { + setupIndex(indexFileName, Level0.class, NoopQueriesAndMutations.class); + Schema schema = createSchema(); + assertThat(schema.containsTypeWithName("Level0"), is(true)); + assertThat(schema.containsTypeWithName("Level1"), is(true)); + assertThat(schema.containsTypeWithName("Level2"), is(true)); + generateGraphQLSchema(schema); + } + + @Test + public void testMultipleLevels() throws IOException, IntrospectionException, ClassNotFoundException { + setupIndex(indexFileName, Level0.class); + + Schema schema = createSchema(); + + assertThat(schema, is(notNullValue())); + assertThat(schema.getTypes().size(), is(6)); + assertThat(schema.getTypeByName("Level0"), is(notNullValue())); + assertThat(schema.getTypeByName("Level1"), is(notNullValue())); + assertThat(schema.getTypeByName("Level2"), is(notNullValue())); + assertThat(schema.getTypeByName("Address"), is(notNullValue())); + assertThat(schema.getTypeByName("Query"), is(notNullValue())); + generateGraphQLSchema(schema); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MapIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MapIT.java new file mode 100644 index 00000000000..eb08d09c3de --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MapIT.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.queries.MapQueries; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.graphql.server.test.types.TypeWithMap; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for handling a {@link Map}. + */ +@AddBean(TypeWithMap.class) +@AddBean(MapQueries.class) +@AddBean(SimpleContact.class) +class MapIT extends AbstractGraphQlCdiIT { + + @Inject + MapIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testMap() throws IOException { + setupIndex(indexFileName, TypeWithMap.class, MapQueries.class, SimpleContact.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("query { query1 { id mapValues mapContacts { id name } } }")); + assertThat(mapResults.size(), is(1)); + + Map mapResults2 = (Map) mapResults.get("query1"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.size(), is(3)); + assertThat(mapResults2.get("id"), is("id1")); + List listString = (List) mapResults2.get("mapValues"); + assertThat(listString.size(), is(2)); + assertThat(listString.get(0), is("one")); + assertThat(listString.get(1), is("two")); + + List> listMapResults3 = (List>) mapResults2.get("mapContacts"); + assertThat(listMapResults3.size(), is(2)); + + Map mapContact1 = listMapResults3.get(0); + Map mapContact2 = listMapResults3.get(1); + assertThat(mapContact1, is(notNullValue())); + assertThat(mapContact1.get("id"), is("c1")); + assertThat(mapContact1.get("name"), is("Tim")); + assertThat(mapContact2, is(notNullValue())); + assertThat(mapContact2.get("id"), is("c2")); + assertThat(mapContact2.get("name"), is("James")); + } + + @Test + public void testMapAsInput() throws IOException { + setupIndex(indexFileName, TypeWithMap.class, MapQueries.class, SimpleContact.class); + InvocationHandler executionContext = createInvocationHandler(); + String input = "{" + + "id: \"id-1\" " + + "mapValues: [ \"a\" \"b\" ] " + + "mapContacts: [{ id: \"c1\" age: 10 name: \"Tim\" tShirtSize: XL } " + + " { id: \"c3\" age: 10 name: \"Tim\" tShirtSize: XL } ] " + + "}"; + Map mapResults = executionContext.execute( + "query { query2(value: " + input + ") { id mapValues mapContacts { id } } }"); + assertThat(mapResults.size(), is(2)); + assertThat(mapResults.get("errors"), is(notNullValue())); + } + + @Test + public void testMapAsInput2() throws IOException { + setupIndex(indexFileName, TypeWithMap.class, MapQueries.class, SimpleContact.class); + InvocationHandler executionContext = createInvocationHandler(); + Map mapResults = executionContext.execute("query { query3 (value: [ \"a\" \"b\" ]) }"); + assertThat(mapResults.size(), is(2)); + assertThat(mapResults.get("errors"), is(notNullValue())); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MultiLevelArraysIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MultiLevelArraysIT.java new file mode 100644 index 00000000000..a44912328f0 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/MultiLevelArraysIT.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.queries.ArrayAndListQueries; +import io.helidon.microprofile.graphql.server.test.types.MultiLevelListsAndArrays; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for Multi-level arrays. + */ +@AddBean(MultiLevelListsAndArrays.class) +@AddBean(ArrayAndListQueries.class) +@AddBean(TestDB.class) +class MultiLevelArraysIT extends AbstractGraphQlCdiIT { + + @Inject + MultiLevelArraysIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + public void testMultipleLevelsOfGenerics() throws IntrospectionException, ClassNotFoundException, IOException { + setupIndex(indexFileName, MultiLevelListsAndArrays.class); + Schema schema = createSchema(); + assertThat(schema.containsTypeWithName("MultiLevelListsAndArrays"), is(true)); + assertThat(schema.containsTypeWithName("Person"), is(true)); + assertThat(schema.containsScalarWithName("BigDecimal"), is(true)); + generateGraphQLSchema(schema); + } + + @Test + public void testMultiLevelPrimitiveArrayAsArgument() throws IOException { + setupIndex(indexFileName, ArrayAndListQueries.class, MultiLevelListsAndArrays.class); + InvocationHandler executionContext = createInvocationHandler(); + Map mapResults = executionContext.execute("query { echo2LevelIntArray(param: [[1, 2], [3, 4]]) }"); + assertThat(mapResults.size(), is(2)); + assertThat(mapResults.get("errors"), is(notNullValue())); + } + + @Test + @SuppressWarnings("unchecked") + public void testMultiLevelListsAndArraysQueries() throws IOException { + setupIndex(indexFileName, ArrayAndListQueries.class, MultiLevelListsAndArrays.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("query { getMultiLevelList { intMultiLevelArray } }")); + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("getMultiLevelList"); + ArrayList> intArrayList = (ArrayList>) mapResults2.get("intMultiLevelArray"); + assertThat(intArrayList, is(notNullValue())); + ArrayList integerArrayList1 = intArrayList.get(0); + assertThat(integerArrayList1, is(notNullValue())); + assertThat(integerArrayList1.contains(1), is(true)); + assertThat(integerArrayList1.contains(2), is(true)); + assertThat(integerArrayList1.contains(3), is(true)); + + ArrayList integerArrayList2 = intArrayList.get(1); + assertThat(integerArrayList2, is(notNullValue())); + assertThat(integerArrayList2.contains(4), is(true)); + assertThat(integerArrayList2.contains(5), is(true)); + assertThat(integerArrayList2.contains(6), is(true)); + + mapResults = getAndAssertResult(executionContext.execute("query { returnListOfStringArrays }")); + assertThat(mapResults.size(), is(1)); + ArrayList> stringArrayList = (ArrayList>) mapResults.get("returnListOfStringArrays"); + assertThat(stringArrayList, is(notNullValue())); + + List stringList1 = stringArrayList.get(0); + assertThat(stringList1, is(notNullValue())); + assertThat(stringList1.contains("one"), is(true)); + assertThat(stringList1.contains("two"), is(true)); + List stringList2 = stringArrayList.get(1); + assertThat(stringList2, is(notNullValue())); + assertThat(stringList2.contains("three"), is(true)); + assertThat(stringList2.contains("four"), is(true)); + assertThat(stringList2.contains("five"), is(true)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoLinkedListBigDecimals(param: [-25.926804, 28.203392]) }")); + assertThat(mapResults.size(), is(1)); + List bigDecimalList = (List) mapResults.get("echoLinkedListBigDecimals"); + assertThat(bigDecimalList, is(notNullValue())); + assertThat(bigDecimalList.get(0), is(BigDecimal.valueOf(-25.926804))); + assertThat(bigDecimalList.get(1), is(new BigDecimal("28.203392"))); + + mapResults = getAndAssertResult(executionContext.execute("query { echoListBigDecimal(param: [-25.926804, 28.203392]) }")); + assertThat(mapResults.size(), is(1)); + bigDecimalList = (List) mapResults.get("echoListBigDecimal"); + assertThat(bigDecimalList, is(notNullValue())); + assertThat(bigDecimalList.get(0), is(BigDecimal.valueOf(-25.926804))); + assertThat(bigDecimalList.get(1), is(BigDecimal.valueOf(28.203392))); + + mapResults = getAndAssertResult(executionContext.execute("query { echoBigDecimalArray(param: [-25.926804, 28.203392]) }")); + assertThat(mapResults.size(), is(1)); + bigDecimalList = (List) mapResults.get("echoBigDecimalArray"); + assertThat(bigDecimalList, is(notNullValue())); + assertThat(bigDecimalList.get(0), is(BigDecimal.valueOf(-25.926804))); + assertThat(bigDecimalList.get(1), is(BigDecimal.valueOf(28.203392))); + + mapResults = getAndAssertResult(executionContext.execute("query { echoIntArray(param: [1, 2]) }")); + assertThat(mapResults.size(), is(1)); + List integerList = (List) mapResults.get("echoIntArray"); + assertThat(integerList, is(notNullValue())); + assertThat(integerList.get(0), is(1)); + assertThat(integerList.get(1), is(2)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoShortArray(param: [1, 2]) }")); + assertThat(mapResults.size(), is(1)); + integerList = (List) mapResults.get("echoShortArray"); + assertThat(integerList, is(notNullValue())); + assertThat(integerList.get(0), is(1)); + assertThat(integerList.get(1), is(2)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoLongArray(param: [1, 2]) }")); + assertThat(mapResults.size(), is(1)); + List listBigInteger = (List) mapResults.get("echoLongArray"); + assertThat(listBigInteger, is(notNullValue())); + assertThat(listBigInteger.get(0), is(BigInteger.valueOf(1))); + assertThat(listBigInteger.get(1), is(BigInteger.valueOf(2))); + + mapResults = getAndAssertResult(executionContext.execute("query { echoDoubleArray(param: [1.1, 2.2]) }")); + assertThat(mapResults.size(), is(1)); + List listDouble = (List) mapResults.get("echoDoubleArray"); + assertThat(listDouble, is(notNullValue())); + assertThat(listDouble.get(0), is(Double.valueOf(1.1))); + assertThat(listDouble.get(1), is(Double.valueOf(2.2))); + + mapResults = getAndAssertResult(executionContext.execute("query { echoBooleanArray(param: [true, false]) }")); + assertThat(mapResults.size(), is(1)); + List listBoolean = (List) mapResults.get("echoBooleanArray"); + assertThat(listBoolean, is(notNullValue())); + assertThat(listBoolean.get(0), is(true)); + assertThat(listBoolean.get(1), is(false)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoCharArray(param: [\"A\", \"B\"]) }")); + assertThat(mapResults.size(), is(1)); + List listString = (List) mapResults.get("echoCharArray"); + assertThat(listString, is(notNullValue())); + assertThat(listString.get(0), is("A")); + assertThat(listString.get(1), is("B")); + + mapResults = getAndAssertResult(executionContext.execute("query { echoFloatArray(param: [1.01, 2.02]) }")); + assertThat(mapResults.size(), is(1)); + listDouble = (List) mapResults.get("echoFloatArray"); + assertThat(listDouble, is(notNullValue())); + assertThat(listDouble.get(0), is(1.01d)); + assertThat(listDouble.get(1), is(2.02d)); + + mapResults = getAndAssertResult(executionContext.execute("query { echoByteArray(param: [0, 1]) }")); + assertThat(mapResults.size(), is(1)); + List listInteger = (List) mapResults.get("echoByteArray"); + assertThat(listInteger, is(notNullValue())); + assertThat(listInteger.get(0), is(0)); + assertThat(listInteger.get(1), is(1)); + + SimpleContact contact1 = new SimpleContact("id1", "name1", 1, EnumTestWithEnumName.XL); + SimpleContact contact2 = new SimpleContact("id2", "name2", 2, EnumTestWithEnumName.S); + + mapResults = getAndAssertResult(executionContext.execute( + "query { echoSimpleContactArray(param: [ " + + generateInput(contact1) + ", " + + generateInput(contact2) + + " ]) { id name age tShirtSize } }")); + assertThat(mapResults.size(), is(1)); + List> listContacts = (List>) mapResults.get("echoSimpleContactArray"); + assertThat(listContacts, is(notNullValue())); + assertThat(listContacts.size(), is(2)); + mapResults2 = listContacts.get(0); + assertThat(mapResults2, is(not(nullValue()))); + assertThat(mapResults2.get("id"), is(contact1.getId())); + assertThat(mapResults2.get("name"), is(contact1.getName())); + assertThat(mapResults2.get("age"), is(contact1.getAge())); + assertThat(mapResults2.get("tShirtSize"), is(contact1.getTShirtSize().toString())); + + mapResults2 = listContacts.get(1); + assertThat(mapResults2, is(not(nullValue()))); + assertThat(mapResults2.get("id"), is(contact2.getId())); + assertThat(mapResults2.get("name"), is(contact2.getName())); + assertThat(mapResults2.get("age"), is(contact2.getAge())); + assertThat(mapResults2.get("tShirtSize"), is(contact2.getTShirtSize().toString())); + + mapResults = getAndAssertResult(executionContext.execute( + "query { processListListBigDecimal(param :[[\"-25.926804 " + + "longlat\", \"28.203392 longlat\"],[\"-26.926804 longlat\", " + + " \"27.203392 longlat\"],[\"-27.926804 longlat\", \"26.203392 longlat\"]] ) }")); + assertThat(mapResults.size(), is(1)); + String result = (String) mapResults.get("processListListBigDecimal"); + assertThat(result, is(notNullValue())); + assertThat(result, is("[[-25.926804, 28.203392], [-26.926804, 27.203392], [-27.926804, 26.203392]]")); + } + + protected String generateInput(SimpleContact contact) { + return new StringBuilder("{") + .append("id: ").append(quote(contact.getId())).append(" ") + .append("name: ").append(quote(contact.getName())).append(" ") + .append("age: ").append(contact.getAge()).append(" ") + .append("tShirtSize: ").append(contact.getTShirtSize()) + .append("}") + .toString(); + + } + + protected String quote(String s) { + return "\"" + s + "\""; + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NullIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NullIT.java new file mode 100644 index 00000000000..a37a5e85132 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NullIT.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.QueriesAndMutationsWithNulls; +import io.helidon.microprofile.graphql.server.test.types.NullPOJO; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for Nulls. + */ +@AddBean(QueriesAndMutationsWithNulls.class) +@AddBean(TestDB.class) +public class NullIT extends AbstractGraphQlCdiIT { + @Inject + public NullIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testNulls() throws IOException { + setupIndex(indexFileName, NullPOJO.class, QueriesAndMutationsWithNulls.class); + InvocationHandler executionContext = createInvocationHandler(); + Schema schema = createSchema(); + assertThat(schema, is(notNullValue())); + + // test primitives should be not null be default + SchemaType type = schema.getTypeByName("NullPOJO"); + assertReturnTypeMandatory(type, "id", true); + assertReturnTypeMandatory(type, "longValue", false); + assertReturnTypeMandatory(type, "stringValue", true); + assertReturnTypeMandatory(type, "testNullWithGet", true); + assertReturnTypeMandatory(type, "listNonNullStrings", false); + assertArrayReturnTypeMandatory(type, "listNonNullStrings", true); + assertArrayReturnTypeMandatory(type, "listOfListOfNonNullStrings", true); + assertReturnTypeMandatory(type, "listOfListOfNonNullStrings", false); + assertReturnTypeMandatory(type, "listOfListOfNullStrings", false); + assertArrayReturnTypeMandatory(type, "listOfListOfNullStrings", false); + assertReturnTypeMandatory(type, "testNullWithSet", false); + assertReturnTypeMandatory(type, "listNullStringsWhichIsMandatory", true); + assertArrayReturnTypeMandatory(type, "listNullStringsWhichIsMandatory", false); + assertReturnTypeMandatory(type, "testInputOnly", false); + assertArrayReturnTypeMandatory(type, "testInputOnly", false); + assertReturnTypeMandatory(type, "testOutputOnly", false); + assertArrayReturnTypeMandatory(type, "testOutputOnly", true); + + SchemaType query = schema.getTypeByName("Query"); + assertReturnTypeMandatory(query, "method1NotNull", true); + assertReturnTypeMandatory(query, "method2NotNull", true); + assertReturnTypeMandatory(query, "method3NotNull", false); + + assertReturnTypeArgumentMandatory(query, "paramShouldBeNonMandatory", "value", false); + assertReturnTypeArgumentMandatory(query, "paramShouldBeNonMandatory2", "value", false); + assertReturnTypeArgumentMandatory(query, "paramShouldBeNonMandatory3", "value", false); + + SchemaType input = schema.getInputTypeByName("NullPOJOInput"); + assertReturnTypeMandatory(input, "nonNullForInput", true); + assertReturnTypeMandatory(input, "testNullWithGet", false); + assertReturnTypeMandatory(input, "testNullWithSet", true); + assertReturnTypeMandatory(input, "listNonNullStrings", false); + assertArrayReturnTypeMandatory(input, "listNonNullStrings", true); + + assertArrayReturnTypeMandatory(input, "listOfListOfNonNullStrings", true); + + assertReturnTypeMandatory(input, "testInputOnly", false); + assertArrayReturnTypeMandatory(input, "testInputOnly", true); + + assertReturnTypeMandatory(input, "testOutputOnly", false); + assertArrayReturnTypeMandatory(input, "testOutputOnly", false); + + Map mapResults = getAndAssertResult(executionContext.execute("mutation { returnNullValues { longValue stringValue } }")); + assertThat(mapResults, is(notNullValue())); + Map mapResults2 = (Map) mapResults.get("returnNullValues"); + assertThat(mapResults2.size(), is(2)); + assertThat(mapResults2.get("longValue"), is(nullValue())); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoNullValue }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoNullValue"), is(nullValue())); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoNullDateValue }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoNullDateValue"), is(nullValue())); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NumberFormatIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NumberFormatIT.java new file mode 100644 index 00000000000..2cfc4d64f68 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/NumberFormatIT.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.NumberFormatQueriesAndMutations; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesWithArgs; +import io.helidon.microprofile.graphql.server.test.types.SimpleContactWithNumberFormats; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATE_SCALAR; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INT; +import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.STRING; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for Number formats. + */ +@AddBean(SimpleContactWithNumberFormats.class) +@AddBean(NumberFormatQueriesAndMutations.class) +@AddBean(SimpleQueriesWithArgs.class) +@AddBean(TestDB.class) +class NumberFormatIT extends AbstractGraphQlCdiIT { + + @Inject + NumberFormatIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testNumberFormats() throws IOException { + setupIndex(indexFileName, SimpleContactWithNumberFormats.class, + NumberFormatQueriesAndMutations.class, SimpleQueriesWithArgs.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult(executionContext + .execute("query { simpleFormattingQuery { id name age " + + "bankBalance value longValue bigDecimal " + + "} }")); + assertThat(mapResults.size(), is(1)); + + Map mapResults2 = (Map) mapResults.get("simpleFormattingQuery"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is("1 id")); + assertThat(mapResults2.get("name"), is("Tim")); + assertThat(mapResults2.get("age"), is("50 years old")); + assertThat(mapResults2.get("bankBalance"), is("$ 1200.00")); + assertThat(mapResults2.get("value"), is("10 value")); + assertThat(mapResults2.get("longValue"), is(BigInteger.valueOf(Long.MAX_VALUE))); + assertThat(mapResults2.get("bigDecimal"), is("BigDecimal-100")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { generateDoubleValue }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("generateDoubleValue"), is("Double-123456789")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { transformedNumber(arg0: 123) }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("transformedNumber"), is("number 123")); + + mapResults = getAndAssertResult(executionContext.execute("query { echoBigDecimalUsingFormat(param1: \"BD-123\") }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoBigDecimalUsingFormat"), is(BigDecimal.valueOf(123.0))); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoBankBalance(bankBalance: \"$ 106,963.87\") }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoBankBalance"), is(Double.valueOf("106963.87"))); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoFloat(size: 10.0123) }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("echoFloat"), is(Double.valueOf("10.0123"))); + + mapResults = getAndAssertResult(executionContext.execute("mutation { idNumber(name: \"Tim\", id: 123) }")); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.get("idNumber"), is("Tim-123")); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoBigDecimalList(coordinates: [ 10.0123, -23.000 ]) }")); + assertThat(mapResults, is(notNullValue())); + List listBigDecimals = (List) mapResults.get("echoBigDecimalList"); + assertThat(listBigDecimals.get(0), is(BigDecimal.valueOf(10.0123))); + assertThat(listBigDecimals.get(1), is(BigDecimal.valueOf(-23.000))); + + + // TODO: COH-21891 + mapResults = getAndAssertResult( + executionContext.execute("query { listAsString(arg1: [ \"value 12.12\", \"value 33.33\"] ) }")); + assertThat(mapResults, is(notNullValue())); + + // create a new contact + String contactInput = + "contact: {" + + "id: 1 " + + "name: \"Tim\" " + + "age: \"20 years old\" " + + "bankBalance: \"$ 1000.01\" " + + "value: \"9 value\" " + + "longValue: \"LongValue-123\"" + + "bigDecimal: \"BigDecimal-12345\"" + + "listOfIntegers: [ \"1 number\", \"2 number\"]" + + " } "; + + mapResults = getAndAssertResult( + executionContext.execute("mutation { createSimpleContactWithNumberFormats (" + contactInput + + ") { id name } }")); + assertThat(mapResults.size(), is(1)); + mapResults2 = (Map) mapResults.get("createSimpleContactWithNumberFormats"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is("1 id")); + assertThat(mapResults2.get("name"), is("Tim")); + + mapResults = getAndAssertResult(executionContext.execute("query { echoFormattedListOfIntegers(value: [ \"1 years old\", \"3 " + + "years old\", \"53 years old\" ]) }")); + assertThat(mapResults, is(notNullValue())); + + List listResults = (List) mapResults.get("echoFormattedListOfIntegers"); + assertThat(listResults.size(), is(3)); + + } + + @Test + public void testCorrectNumberScalarTypesAndFormats() throws IOException { + setupIndex(indexFileName, SimpleContactWithNumberFormats.class, NumberFormatQueriesAndMutations.class); + Schema schema = createSchema(); + + // validate the formats on the type + SchemaType type = schema.getTypeByName("SimpleContactWithNumberFormats"); + assertThat(type, is(notNullValue())); + + SchemaFieldDefinition fd = getFieldDefinition(type, "id"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("0 'id'")); + assertThat(fd.returnType(), is(STRING)); + + fd = getFieldDefinition(type, "age"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("0 'years old'")); + assertThat(fd.returnType(), is(STRING)); + + // validate the formats on the Input Type + SchemaType inputType = schema.getInputTypeByName("SimpleContactWithNumberFormatsInput"); + assertThat(inputType, is(notNullValue())); + fd = getFieldDefinition(inputType, "id"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is(nullValue())); + assertThat(fd.returnType(), is(INT)); + + fd = getFieldDefinition(inputType, "longValue"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("LongValue-##########")); + assertThat(fd.returnType(), is(STRING)); + + fd = getFieldDefinition(inputType, "age"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("0 'years old'")); + assertThat(fd.returnType(), is(STRING)); + + fd = getFieldDefinition(inputType, "listDates"); + assertThat(fd, is(notNullValue())); + assertThat(fd.format()[0], is("DD-MM-YYYY")); + assertThat(fd.returnType(), is(FORMATTED_DATE_SCALAR)); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PartialResultsExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PartialResultsExceptionIT.java new file mode 100644 index 00000000000..7989b8b5270 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PartialResultsExceptionIT.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static io.helidon.graphql.server.GraphQlConstants.DATA; +import static io.helidon.graphql.server.GraphQlConstants.ERRORS; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for partial results with exception. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +class PartialResultsExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + protected PartialResultsExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimplePartialResults() throws IOException { + setupIndex(indexFileName, ExceptionQueries.class); + + InvocationHandler executionContext = createInvocationHandler(); + + Map results = executionContext.execute("query { failAfterNResults(failAfter: 4) }"); + assertThat(results.size(), is(2)); + + List> listErrors = (List>) results.get(ERRORS); + assertThat(listErrors.size(), is(1)); + Map mapErrors = (Map) listErrors.get(0); + + // since this is a checked exception we should see message + assertThat(mapErrors.get("message"), is("Partial results")); + Map mapData = (Map) results.get(DATA); + assertThat(mapData.size(), is(1)); + List listIntegers = (List) mapData.get("failAfterNResults"); + assertThat(listIntegers.size(), is(4)); + } + + @Test + public void testComplexPartialResults() throws IOException { + setupIndex(indexFileName, ExceptionQueries.class, SimpleContact.class); + + InvocationHandler executionContext = createInvocationHandler(); + + Map results = executionContext.execute( + "query { failAfterNContacts(failAfter: 4) { id name age } }"); + assertThat(results.size(), is(2)); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PojoNamingIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PojoNamingIT.java new file mode 100644 index 00000000000..f7f6d3d2d18 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PojoNamingIT.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.beans.IntrospectionException; +import java.io.IOException; + +import io.helidon.microprofile.graphql.server.test.queries.NoopQueriesAndMutations; +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.types.Person; +import io.helidon.microprofile.graphql.server.test.types.PersonWithName; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for naming of Pojo's. + */ +@AddBean(Person.class) +@AddBean(NoopQueriesAndMutations.class) +class PojoNamingIT extends AbstractGraphQlCdiIT { + + @Inject + PojoNamingIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + /** + * Test generation of Type with no-name. + */ + @Test + public void testTypeGenerationWithNoName() throws IntrospectionException, ClassNotFoundException, IOException { + setupIndex(indexFileName, Person.class, NoopQueriesAndMutations.class); + Schema schema = createSchema(); + assertThat(schema.getTypeByName("Person"), is(notNullValue())); + assertThat(schema.getTypeByName("Address"), is(notNullValue())); + assertThat(schema.containsScalarWithName("Date"), is(notNullValue())); + assertThat(schema.containsScalarWithName("BigDecimal"), is(notNullValue())); + generateGraphQLSchema(schema); + } + + /** + * Test generation of Type with a different name then class name. + */ + @Test + public void testPersonWithName() throws IOException, IntrospectionException, ClassNotFoundException { + setupIndex(indexFileName, PersonWithName.class, NoopQueriesAndMutations.class); + Schema schema = createSchema(); + + assertThat(schema, is(notNullValue())); + assertThat(schema.getTypeByName("Person"), is(notNullValue())); + generateGraphQLSchema(schema); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PropertyNameIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PropertyNameIT.java new file mode 100644 index 00000000000..9e584013667 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/PropertyNameIT.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.queries.PropertyNameQueries; +import io.helidon.microprofile.graphql.server.test.types.TypeWithNameAndJsonbProperty; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for property naming. + */ +@AddBean(PropertyNameQueries.class) +@AddBean(TestDB.class) +public class PropertyNameIT extends AbstractGraphQlCdiIT { + @Inject + public PropertyNameIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + @Test + @SuppressWarnings("unchecked") + public void testDifferentPropertyNames() throws IOException { + setupIndex(indexFileName, PropertyNameQueries.class, TypeWithNameAndJsonbProperty.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("query { query1 { newFieldName1 newFieldName2 " + + " newFieldName3 newFieldName4 newFieldName5 newFieldName6 } }")); + assertThat(mapResults.size(), is(1)); + + Map mapResults2 = (Map) mapResults.get("query1"); + assertThat(mapResults2.size(), is(6)); + for (int i = 1; i <= 6; i++) { + assertThat(mapResults2.get("newFieldName" + i), is("name" + i)); + } + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleMutationsIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleMutationsIT.java new file mode 100644 index 00000000000..d690cc92990 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleMutationsIT.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.mutations.SimpleMutations; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for simple mutations. + */ +@AddBean(SimpleMutations.class) +@AddBean(TestDB.class) +class SimpleMutationsIT extends AbstractGraphQlCdiIT { + + @Inject + SimpleMutationsIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleMutations() throws IOException { + setupIndex(indexFileName, SimpleMutations.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult( + executionContext.execute("mutation { createNewContact { id name age } }")); + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("createNewContact"); + + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is(notNullValue())); + assertThat(mapResults2.get("name"), is(notNullValue())); + assertThat(((String) mapResults2.get("name")).startsWith("Name"), is(true)); + assertThat(mapResults2.get("age"), is(notNullValue())); + + mapResults = getAndAssertResult( + executionContext.execute("mutation { createContactWithName(name: \"tim\") { id name age } }")); + assertThat(mapResults.size(), is(1)); + mapResults2 = (Map) mapResults.get("createContactWithName"); + + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is(notNullValue())); + assertThat(mapResults2.get("name"), is("tim")); + assertThat(mapResults2.get("age"), is(notNullValue())); + + mapResults = getAndAssertResult(executionContext.execute("mutation { echoStringValue(value: \"echo\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("echoStringValue"), is("echo")); + + mapResults = getAndAssertResult(executionContext.execute( + "mutation { testStringArrays(places: [\"place1\", \"place2\", \"place3\"]) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("testStringArrays"), is("place1place2place3")); + + mapResults = getAndAssertResult( + executionContext.execute("mutation { createAndReturnNewContact(newContact: { name: \"tim\", age: 22, id: \"1\", tShirtSize: XL } ) { id name age tShirtSize } }")); + assertThat(mapResults.size(), is(1)); + mapResults2 = (Map) mapResults.get("createAndReturnNewContact"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("name"), is("tim")); + assertThat(mapResults2.get("age"), is(22)); + assertThat(mapResults2.get("id"), is("1")); + assertThat(mapResults2.get("tShirtSize"), is("XL")); + + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleQueriesWithArgsIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleQueriesWithArgsIT.java new file mode 100644 index 00000000000..cead7dc8dd0 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SimpleQueriesWithArgsIT.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.math.BigInteger; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithNameAnnotation; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesWithArgs; +import io.helidon.microprofile.graphql.server.test.types.AbstractVehicle; +import io.helidon.microprofile.graphql.server.test.types.Car; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * Tests for simple queries with args. + */ +@AddBean(SimpleQueriesWithArgs.class) +@AddBean(TestDB.class) +public class SimpleQueriesWithArgsIT extends AbstractGraphQlCdiIT { + + @Inject + SimpleQueriesWithArgsIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleQueryGenerationWithArgs() throws IOException { + setupIndex(indexFileName, SimpleQueriesWithArgs.class, Car.class, AbstractVehicle.class); + InvocationHandler executionContext = createInvocationHandler(); + + Map mapResults = getAndAssertResult(executionContext.execute("query { hero(heroType: \"human\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("hero"), is("Luke")); + + mapResults = getAndAssertResult(executionContext.execute("query { findLocalDates(numberOfValues: 10) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("findLocalDates"), is(notNullValue())); + List listLocalDate = (List) mapResults.get("findLocalDates"); + assertThat(listLocalDate.size(), is(10)); + + mapResults = getAndAssertResult( + executionContext.execute("query { canFindContact(contact: { id: \"10\" name: \"tim\" age: 52 }) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("canFindContact"), is(false)); + + mapResults = getAndAssertResult(executionContext.execute("query { hero(heroType: \"droid\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("hero"), is("R2-D2")); + + mapResults = getAndAssertResult(executionContext.execute("query { multiply(arg0: 10, arg1: 10) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("multiply"), is(BigInteger.valueOf(100))); + + mapResults = getAndAssertResult(executionContext + .execute( + "query { findAPerson(personId: 1) { personId creditLimit workAddress { " + + "city state zipCode } } }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("findAPerson"), is(notNullValue())); + + mapResults = getAndAssertResult(executionContext.execute("query { findEnums(arg0: S) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("findEnums"), is(notNullValue())); + + mapResults = getAndAssertResult(executionContext.execute("query { getMonthFromDate(date: \"2020-12-20\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("getMonthFromDate"), is("DECEMBER")); + + mapResults = getAndAssertResult(executionContext.execute("query { findOneEnum(enum: XL) }")); + assertThat(mapResults.size(), is(1)); + Collection listEnums = (Collection) mapResults.get("findOneEnum"); + assertThat(listEnums.size(), is(1)); + assertThat(listEnums.iterator().next(), is(EnumTestWithNameAnnotation.XL.toString())); + + mapResults = getAndAssertResult(executionContext.execute("query { returnIntegerAsId(param1: 123) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnIntegerAsId"), is(123)); + + mapResults = getAndAssertResult(executionContext.execute("query { returnIntAsId(param1: 124) }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnIntAsId"), is(124)); + + mapResults = getAndAssertResult(executionContext.execute("query { returnStringAsId(param1: \"StringValue\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnStringAsId"), is("StringValue")); + + mapResults = getAndAssertResult(executionContext.execute("query { returnLongAsId(param1: " + Long.MAX_VALUE + ") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnLongAsId"), is(BigInteger.valueOf(Long.MAX_VALUE))); + + mapResults = getAndAssertResult( + executionContext.execute("query { returnLongPrimitiveAsId(param1: " + Long.MAX_VALUE + ") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnLongPrimitiveAsId"), is(BigInteger.valueOf(Long.MAX_VALUE))); + + UUID uuid = UUID.randomUUID(); + mapResults = getAndAssertResult(executionContext.execute("query { returnUUIDAsId(param1: \"" + + uuid.toString() + "\") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("returnUUIDAsId"), is(uuid.toString())); + + mapResults = getAndAssertResult(executionContext.execute( + "query { findPeopleFromState(state: \"MA\") { personId creditLimit workAddress { city state zipCode } } }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("findPeopleFromState"), is(notNullValue())); + ArrayList> arrayList = (ArrayList>) mapResults.get("findPeopleFromState"); + assertThat(arrayList, is(notNullValue())); + + mapResults = getAndAssertResult(executionContext.execute( + "query { findPeopleFromState(state: \"MA\") { personId creditLimit workAddress { city state zipCode } } }")); + assertThat(mapResults.size(), is(1)); + + SimpleContact contact1 = new SimpleContact("c1", "Contact 1", 50, EnumTestWithEnumName.L); + SimpleContact contact2 = new SimpleContact("c2", "Contact 2", 53, EnumTestWithEnumName.S); + + String json = "relationship: {" + + " contact1: " + getContactAsQueryInput(contact1) + + " contact2: " + getContactAsQueryInput(contact2) + + " relationship: \"married\"" + + "}"; + mapResults = getAndAssertResult(executionContext.execute("query { canFindContactRelationship( " + + json + + ") }")); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("canFindContactRelationship"), is(false)); + + // validate that the name for the enum for SimpleContact is correct + Schema schema = createSchema(); + SchemaType type = schema.getTypeByName("SimpleContact"); + assertThat(type, is(notNullValue())); + assertThat(type.getFieldDefinitionByName("tShirtSize"), is(notNullValue())); + } + + @Test + public void testQueriesWithVariables() throws IOException { + setupIndex(indexFileName, SimpleQueriesWithArgs.class); + InvocationHandler executionContext = createInvocationHandler(); + Map mapVariables = Map.of("first", 10, "second", 20); + Map mapResults = getAndAssertResult(executionContext.execute( + "query additionQuery($first: Int!, $second: Int!) {" + + " additionQuery(value1: $first, value2: $second) }", "additionQuery", mapVariables)); + assertThat(mapResults, is(notNullValue())); + assertThat(mapResults.size(), is(1)); + assertThat(mapResults.get("additionQuery"), is(30)); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SourceIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SourceIT.java new file mode 100644 index 00000000000..6944f6bda1c --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/SourceIT.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; + +import io.helidon.graphql.server.InvocationHandler; +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.enums.EnumTestWithEnumName; +import io.helidon.microprofile.graphql.server.test.queries.SimpleQueriesWithSource; +import io.helidon.microprofile.graphql.server.test.types.SimpleContact; +import io.helidon.microprofile.tests.junit5.AddBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for Source annotation. + */ +@AddBean(SimpleQueriesWithSource.class) +@AddBean(TestDB.class) +class SourceIT extends AbstractGraphQlCdiIT { + + @Inject + SourceIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleQueriesWithSource() throws IOException { + setupIndex(indexFileName, SimpleQueriesWithSource.class, SimpleContact.class); + InvocationHandler executionContext = createInvocationHandler(); + + // since there is a @Source annotation in SimpleQueriesWithSource, then this should add a field + // idAndName to the SimpleContact type + Map mapResults = getAndAssertResult(executionContext.execute("query { findContact { id idAndName } }")); + + assertThat(mapResults.size(), is(1)); + Map mapResults2 = (Map) mapResults.get("findContact"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is(notNullValue())); + assertThat(mapResults2.get("idAndName"), is(notNullValue())); + + // test the query at the top level + SimpleContact contact1 = new SimpleContact("c1", "Contact 1", 50, EnumTestWithEnumName.XL); + + String json = "contact: " + getContactAsQueryInput(contact1); + + mapResults = getAndAssertResult(executionContext.execute("query { currentJob (" + json + ") }")); + assertThat(mapResults.size(), is(1)); + String currentJob = (String) mapResults.get("currentJob"); + assertThat(currentJob, is(notNullValue())); + + // test the query from the object + mapResults = getAndAssertResult(executionContext.execute("query { findContact { id idAndName currentJob } }")); + assertThat(mapResults.size(), is(1)); + mapResults2 = (Map) mapResults.get("findContact"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is(notNullValue())); + assertThat(mapResults2.get("idAndName"), is(notNullValue())); + assertThat(mapResults2.get("currentJob"), is(notNullValue())); + + // test the query from the object + mapResults = getAndAssertResult(executionContext.execute("query { findContact { id lastNAddress(count: 1) { city } } }")); + assertThat(mapResults.size(), is(1)); + mapResults2 = (Map) mapResults.get("findContact"); + assertThat(mapResults2, is(notNullValue())); + assertThat(mapResults2.get("id"), is(notNullValue())); + assertThat(mapResults2.get("lastNAddress"), is(notNullValue())); + } + +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidMutationsIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidMutationsIT.java new file mode 100644 index 00000000000..e9ae1a690aa --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidMutationsIT.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.mutations.VoidMutations; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for void mutations. + */ +class VoidMutationsIT extends AbstractGraphQlIT { + + VoidMutationsIT() { + super(Set.of(VoidMutations.class)); + } + + @Test + void testVoidMutations() throws IOException { + setupIndex(indexFileName, VoidMutations.class); + assertThrows(RuntimeException.class, () -> createInvocationHandler()); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidQueriesIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidQueriesIT.java new file mode 100644 index 00000000000..84572b58d9d --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/VoidQueriesIT.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.microprofile.graphql.server.test.queries.VoidQueries; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for void queries. + */ +class VoidQueriesIT extends AbstractGraphQlIT { + + VoidQueriesIT() { + super(Set.of(VoidQueries.class)); + } + + @Test + void testVoidQueries() throws IOException { + setupIndex(indexFileName, VoidQueries.class); + assertThrows(RuntimeException.class, this::createInvocationHandler); + } +} diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/WLOfCheckedExceptionIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/WLOfCheckedExceptionIT.java new file mode 100644 index 00000000000..a6dc93d55e2 --- /dev/null +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/WLOfCheckedExceptionIT.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.graphql.server; + +import java.io.IOException; + +import javax.inject.Inject; + +import io.helidon.microprofile.graphql.server.test.db.TestDB; +import io.helidon.microprofile.graphql.server.test.exception.ExceptionQueries; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddConfig; + +import org.eclipse.microprofile.graphql.ConfigKey; +import org.junit.jupiter.api.Test; + + +/** + * Tests for allow list of checked exceptions. + */ +@AddBean(ExceptionQueries.class) +@AddBean(TestDB.class) +@AddConfig(key = ConfigKey.EXCEPTION_WHITE_LIST, value = "java.lang.IllegalArgumentException") +class WLOfCheckedExceptionIT extends AbstractGraphQlCdiIT { + + @Inject + protected WLOfCheckedExceptionIT(GraphQlCdiExtension graphQlCdiExtension) { + super(graphQlCdiExtension); + } + + @Test + void testWhiteListOfCheckedException() throws IOException { + setupIndex(indexFileName, ExceptionQueries.class); + assertMessageValue("query { uncheckedQuery1 }", + "java.security.AccessControlException: my exception", true); + assertMessageValue("query { uncheckedQuery2 }", + "java.security.AccessControlException: my exception", true); + } +} diff --git a/tests/integration/mp-graphql/src/test/resources/META-INF/beans.xml b/tests/integration/mp-graphql/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e5a9e54aa5e --- /dev/null +++ b/tests/integration/mp-graphql/src/test/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/tests/integration/mp-graphql/src/test/resources/META-INF/microprofile-config.properties b/tests/integration/mp-graphql/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..cd865c544ee --- /dev/null +++ b/tests/integration/mp-graphql/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mp.initializer.allow=true +mp.initializer.no-warn=true diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 72494beccfc..55af226d370 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -35,6 +35,7 @@ zipkin-mp-2.2 mp-grpc + mp-graphql mp-security-client health mp-ws-services