From cc44deee1ab4fe494afd5bdc5483124f9cf3e91b Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 22 Feb 2022 09:48:57 -0500 Subject: [PATCH] GraphQL Introspection Support (#3348) * Start working on graphql Schema * Start testing * Work on introspection * Work on introspection * Use integers for date ordinals * Add changelog * GraphQL updates * Ongoing wrk * Cleanup * Bump core lib * Add changelog * Clean up dependencies * CLeanup * Add missing message * Test fix * Change to force CI --- .../src/main/java/ca/uhn/fhir/i18n/Msg.java | 2 +- .../ca/uhn/fhir/util/MessageSupplier.java | 24 ++ .../java/ca/uhn/fhir/util/StringUtil.java | 10 + .../ca/uhn/fhir/util/MessageSupplierTest.java | 64 +++++ .../java/ca/uhn/fhir/util/StringUtilTest.java | 5 + .../6_0_0/3348-add-graphql-introspection.yaml | 5 + hapi-fhir-jpaserver-base/pom.xml | 24 +- .../fhir/jpa/config/dstu3/JpaDstu3Config.java | 7 +- .../uhn/fhir/jpa/config/r4/JpaR4Config.java | 47 +-- .../uhn/fhir/jpa/config/r5/JpaR5Config.java | 7 +- ...JpaPersistedResourceValidationSupport.java | 3 + .../GraphQLProviderWithIntrospection.java | 269 ++++++++++++++++++ .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 10 - .../dao/r4/FhirResourceDaoR4CreateTest.java | 5 + ...erTest.java => GraphQLR4ProviderTest.java} | 82 ++---- .../dstu3/GraphQLProviderDstu3Test.java | 4 +- .../provider/r4/GraphQLProviderR4Test.java | 93 ------ .../fhir/jpa/provider/r4/GraphQLR4Test.java | 227 +++++++++++++++ hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 7 + .../ca/uhn/fhirtest/TestRestfulServer.java | 3 + .../uhn/fhirtest/config/TestDstu2Config.java | 25 +- .../uhn/fhirtest/config/TestDstu3Config.java | 29 +- .../ca/uhn/fhirtest/config/TestR4Config.java | 24 +- .../ca/uhn/fhirtest/config/TestR5Config.java | 31 +- .../uhn/fhir/jpa/graphql/GraphQLProvider.java | 48 ++-- .../rest/client/LoggingInterceptorTest.java | 2 +- .../fhir/rest/server/GraphQLR4RawTest.java | 134 +++++---- .../ca/uhn/fhir/util/GraphQLEngineTest.java | 51 +++- .../hl7/fhir/r5/utils/GraphQLEngineTest.java | 184 ++++++++++++ pom.xml | 16 +- 30 files changed, 1082 insertions(+), 360 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MessageSupplier.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/util/MessageSupplierTest.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3348-add-graphql-introspection.yaml create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProviderWithIntrospection.java rename hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/{JpaGraphQLR4ProviderTest.java => GraphQLR4ProviderTest.java} (79%) delete mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLProviderR4Test.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLR4Test.java create mode 100644 hapi-fhir-structures-r5/src/test/java/org/hl7/fhir/r5/utils/GraphQLEngineTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java index c58fd07bc8e..7b0db303275 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java @@ -25,7 +25,7 @@ public final class Msg { /** * IMPORTANT: Please update the following comment after you add a new code - * Last code value: 2034 + * Last code value: 2036 */ private Msg() {} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MessageSupplier.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MessageSupplier.java new file mode 100644 index 00000000000..dc3b7426ef3 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MessageSupplier.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.util; + +import java.util.function.Supplier; + +/** + * This is used to allow lazy parameter initialization for SLF4j - Hopefully + * a future version will allow lambda params + */ +public class MessageSupplier { + private Supplier supplier; + + public MessageSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public String toString() { + return supplier.get().toString(); + } + + public static MessageSupplier msg(Supplier supplier) { + return new MessageSupplier(supplier); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java index b673d89fa81..9da62fbd99e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.util; * #L% */ +import javax.annotation.Nonnull; import java.io.CharArrayWriter; import java.nio.charset.StandardCharsets; import java.text.Normalizer; @@ -105,4 +106,13 @@ public class StringUtil { return theString.substring(0, theString.offsetByCodePoints(0, theCodePointCount)); } + @Nonnull + public static String prependLineNumbers(@Nonnull String theInput) { + StringBuilder schemaOutput = new StringBuilder(); + int index = 0; + for (String next : theInput.split("\\n")) { + schemaOutput.append(index++).append(": ").append(next.replace("\r", "")).append("\n"); + } + return schemaOutput.toString(); + } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/MessageSupplierTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/MessageSupplierTest.java new file mode 100644 index 00000000000..8dd2861c56f --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/MessageSupplierTest.java @@ -0,0 +1,64 @@ +package ca.uhn.fhir.util; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static ca.uhn.fhir.util.MessageSupplier.msg; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MessageSupplierTest { + + private static final Logger ourLog = LoggerFactory.getLogger(MessageSupplierTest.class); + private Appender myMockAppender; + private ch.qos.logback.classic.Logger myLoggerRoot; + + @AfterEach + public void after() { + myLoggerRoot.detachAppender(myMockAppender); + } + + @SuppressWarnings("unchecked") + @BeforeEach + public void before() { + + /* + * This is a bit funky, but it's useful for verifying that the headers actually get logged + */ + myMockAppender = mock(Appender.class); + when(myMockAppender.getName()).thenReturn("MOCK"); + + Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + myLoggerRoot = (ch.qos.logback.classic.Logger) logger; + myLoggerRoot.addAppender(myMockAppender); + } + + + @Test + public void testLog() { + + ourLog.info("Hello: {}", msg(() -> "Goodbye")); + + verify(myMockAppender, times(1)).doAppend(argThat((ArgumentMatcher) argument -> { + String formattedMessage = argument.getFormattedMessage(); + System.out.flush(); + System.out.println("** Got Message: " + formattedMessage); + System.out.flush(); + return formattedMessage.equals("Hello: Goodbye"); + })); + + + } + + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StringUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StringUtilTest.java index 197181d5949..58ec823e766 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StringUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StringUtilTest.java @@ -64,4 +64,9 @@ public class StringUtilTest { assertEquals("a/a", StringUtil.chompCharacter("a/a////", '/')); } + @Test + public void testPrependLineNumbers() { + assertEquals("0: A\n1: B\n", StringUtil.prependLineNumbers("A\nB")); + } + } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3348-add-graphql-introspection.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3348-add-graphql-introspection.yaml new file mode 100644 index 00000000000..a9878fa978a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3348-add-graphql-introspection.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 3348 +title: "The HAPI FHIR server GraphQL endpoints now support GraphQL introspection, making them + much easier to use with GraphQL-capable IDEs." diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 9d9cbdf11bc..d5ecae38de0 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -15,24 +15,6 @@ HAPI FHIR JPA Server - com.fasterxml.woodstox @@ -174,6 +156,12 @@ javassist + + + com.graphql-java + graphql-java + + com.healthmarketscience.sqlbuilder diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/JpaDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/JpaDstu3Config.java index 7765567f550..101ceb40c37 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/JpaDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/JpaDstu3Config.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config.dstu3; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.api.IDaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.GeneratedDaoAndResourceProviderConfigDstu3; import ca.uhn.fhir.jpa.config.JpaConfig; @@ -9,6 +10,7 @@ import ca.uhn.fhir.jpa.config.SharedConfigDstu3Plus; import ca.uhn.fhir.jpa.dao.ITransactionProcessorVersionAdapter; import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; +import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection; import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; import ca.uhn.fhir.jpa.term.TermReadSvcDstu3; import ca.uhn.fhir.jpa.term.TermVersionAdapterSvcDstu3; @@ -17,6 +19,7 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvcDstu3; import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Meta; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; @@ -62,8 +65,8 @@ public class JpaDstu3Config { @Bean(name = JpaConfig.GRAPHQL_PROVIDER_NAME) @Lazy - public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport) { - return new GraphQLProvider(theFhirContext, theValidationSupport, theGraphqlStorageServices); + public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry, IDaoRegistry theDaoRegistry) { + return new GraphQLProviderWithIntrospection(theFhirContext, theValidationSupport, theGraphqlStorageServices, theSearchParamRegistry, theDaoRegistry); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 857e499010c..4c4872e904f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.api.IDaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.GeneratedDaoAndResourceProviderConfigR4; import ca.uhn.fhir.jpa.config.JpaConfig; @@ -9,6 +10,7 @@ import ca.uhn.fhir.jpa.config.SharedConfigDstu3Plus; import ca.uhn.fhir.jpa.dao.ITransactionProcessorVersionAdapter; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; +import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection; import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; import ca.uhn.fhir.jpa.term.TermReadSvcR4; import ca.uhn.fhir.jpa.term.TermVersionAdapterSvcR4; @@ -17,6 +19,7 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4; import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; @@ -26,6 +29,26 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.transaction.annotation.EnableTransactionManagement; +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * 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. + * #L% + */ + @Configuration @EnableTransactionManagement @Import({ @@ -48,8 +71,8 @@ public class JpaR4Config { @Bean(name = JpaConfig.GRAPHQL_PROVIDER_NAME) @Lazy - public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport) { - return new GraphQLProvider(theFhirContext, theValidationSupport, theGraphqlStorageServices); + public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry, IDaoRegistry theDaoRegistry) { + return new GraphQLProviderWithIntrospection(theFhirContext, theValidationSupport, theGraphqlStorageServices, theSearchParamRegistry, theDaoRegistry); } @Bean(name = "mySystemDaoR4") @@ -77,23 +100,3 @@ public class JpaR4Config { } } - -/* - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2022 Smile CDR, Inc. - * %% - * 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. - * #L% - */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/JpaR5Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/JpaR5Config.java index d9b1f03bf27..2d39efdd327 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/JpaR5Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/JpaR5Config.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config.r5; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.api.IDaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.GeneratedDaoAndResourceProviderConfigR5; import ca.uhn.fhir.jpa.config.JpaConfig; @@ -9,6 +10,7 @@ import ca.uhn.fhir.jpa.config.SharedConfigDstu3Plus; import ca.uhn.fhir.jpa.dao.ITransactionProcessorVersionAdapter; import ca.uhn.fhir.jpa.dao.r5.TransactionProcessorVersionAdapterR5; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; +import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection; import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; import ca.uhn.fhir.jpa.term.TermReadSvcR5; import ca.uhn.fhir.jpa.term.TermVersionAdapterSvcR5; @@ -17,6 +19,7 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR5; import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Meta; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; @@ -68,8 +71,8 @@ public class JpaR5Config { @Bean(name = JpaConfig.GRAPHQL_PROVIDER_NAME) @Lazy - public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport) { - return new GraphQLProvider(theFhirContext, theValidationSupport, theGraphqlStorageServices); + public GraphQLProvider graphQLProvider(FhirContext theFhirContext, IGraphQLStorageServices theGraphqlStorageServices, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry, IDaoRegistry theDaoRegistry) { + return new GraphQLProviderWithIntrospection(theFhirContext, theValidationSupport, theGraphqlStorageServices, theSearchParamRegistry, theDaoRegistry); } @Bean(name = "mySystemDaoR5") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index e0ae5d220eb..37af4d20bef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -161,6 +161,9 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport @Nullable @Override public List fetchAllStructureDefinitions() { + if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) { + return null; + } IBundleProvider search = myDaoRegistry.getResourceDao("StructureDefinition").search(new SearchParameterMap().setLoadSynchronousUpTo(1000)); return (List) search.getResources(0, 1000); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProviderWithIntrospection.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProviderWithIntrospection.java new file mode 100644 index 00000000000..6120f2c5f65 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProviderWithIntrospection.java @@ -0,0 +1,269 @@ +package ca.uhn.fhir.jpa.graphql; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * 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. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.IDaoRegistry; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.util.StringUtil; +import ca.uhn.fhir.util.VersionUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.scalar.GraphqlStringCoercing; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.apache.commons.io.output.StringBuilderWriter; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.SearchParameter; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.utils.GraphQLSchemaGenerator; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.util.MessageSupplier.msg; + +public class GraphQLProviderWithIntrospection extends GraphQLProvider { + + private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProviderWithIntrospection.class); + private final GraphQLSchemaGenerator myGenerator; + private final ISearchParamRegistry mySearchParamRegistry; + private final VersionSpecificWorkerContextWrapper myContext; + private final IDaoRegistry myDaoRegistry; + + /** + * Constructor + */ + public GraphQLProviderWithIntrospection(FhirContext theFhirContext, IValidationSupport theValidationSupport, IGraphQLStorageServices theIGraphQLStorageServices, ISearchParamRegistry theSearchParamRegistry, IDaoRegistry theDaoRegistry) { + super(theFhirContext, theValidationSupport, theIGraphQLStorageServices); + + mySearchParamRegistry = theSearchParamRegistry; + myDaoRegistry = theDaoRegistry; + + myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport); + myGenerator = new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion()); + } + + @Override + public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQueryUrl) { + return super.processGraphQlGetRequest(theRequestDetails, theId, theQueryUrl); + } + + @Override + public String processGraphQlPostRequest(ServletRequestDetails theServletRequestDetails, RequestDetails theRequestDetails, IIdType theId, String theQueryBody) { + if (theQueryBody.contains("__schema")) { + EnumSet operations; + if (theId != null) { + throw new InvalidRequestException(Msg.code(2035) + "GraphQL introspection not supported at instance level. Please try at server- or instance- level."); + } + + operations = EnumSet.of(GraphQLSchemaGenerator.FHIROperationType.READ, GraphQLSchemaGenerator.FHIROperationType.SEARCH); + + Collection resourceTypes; + if (theRequestDetails.getResourceName() != null) { + resourceTypes = Collections.singleton(theRequestDetails.getResourceName()); + } else { + resourceTypes = new HashSet<>(); + for (String next : myContext.getResourceNames()) { + if (myDaoRegistry.isResourceTypeSupported(next)) { + resourceTypes.add(next); + } + } + resourceTypes = resourceTypes + .stream() + .sorted() + .collect(Collectors.toList()); + } + + return generateSchema(theQueryBody, resourceTypes, operations); + } else { + return super.processGraphQlPostRequest(theServletRequestDetails, theRequestDetails, theId, theQueryBody); + } + } + + private String generateSchema(String theQueryBody, Collection theResourceTypes, EnumSet theOperations) { + + final StringBuilder schemaBuilder = new StringBuilder(); + try (Writer writer = new StringBuilderWriter(schemaBuilder)) { + // Generate FHIR base types schemas + myGenerator.generateTypes(writer, theOperations); + + // Fix up a few things that are missing from the generated schema + writer + .append("\ntype Resource {") + .append("\n id: [token]" + "\n}") + .append("\n"); + writer + .append("\ninput ResourceInput {") + .append("\n id: [token]" + "\n}") + .append("\n"); + + // Generate schemas for the resource types + for (String nextResourceType : theResourceTypes) { + StructureDefinition sd = fetchStructureDefinition(nextResourceType); + List parameters = toR5SearchParams(mySearchParamRegistry.getActiveSearchParams(nextResourceType).values()); + myGenerator.generateResource(writer, sd, parameters, theOperations); + } + + // Generate queries + writer.append("\ntype Query {"); + for (String nextResourceType : theResourceTypes) { + if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) { + writer + .append("\n ") + .append(nextResourceType) + .append("(id: String): ") + .append(nextResourceType) + .append("\n"); + } + if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) { + List parameters = toR5SearchParams(mySearchParamRegistry.getActiveSearchParams(nextResourceType).values()); + myGenerator.generateListAccessQuery(writer, parameters, nextResourceType); + myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType); + } + } + writer.append("\n}"); + + writer.flush(); + } catch (IOException e) { + throw new InternalErrorException(Msg.code(2036) + e.getMessage(), e); + } + + String schema = schemaBuilder.toString().replace("\r", ""); + + // Set these to INFO if you're testing, then set back before committing + ourLog.debug("Schema generated: {} chars", schema.length()); + ourLog.debug("Schema generated: {}", msg(() -> StringUtil.prependLineNumbers(schema))); + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); + for (String next : typeDefinitionRegistry.scalars().keySet()) { + if (Character.isUpperCase(next.charAt(0))) { + // Skip GraphQL built-in types + continue; + } + runtimeWiringBuilder.scalar(new GraphQLScalarType.Builder().name(next).coercing(new GraphqlStringCoercing()).build()); + } + + RuntimeWiring runtimeWiring = runtimeWiringBuilder.build(); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + GraphQL build = GraphQL.newGraphQL(graphQLSchema).build(); + ExecutionResult executionResult = build.execute(theQueryBody); + + Map data = executionResult.toSpecification(); + Gson gson = new GsonBuilder().create(); + return gson.toJson(data); + + } + + @Nonnull + private List toR5SearchParams(Collection searchParams) { + List parameters = new ArrayList<>(); + for (RuntimeSearchParam next : searchParams) { + SearchParameter sp = toR5SearchParam(next); + if (sp != null) { + parameters.add(sp); + } + } + return parameters; + } + + @Nullable + private SearchParameter toR5SearchParam(RuntimeSearchParam next) { + SearchParameter sp = new SearchParameter(); + sp.setUrl(next.getUri()); + sp.setCode(next.getName()); + sp.setName(next.getName()); + + switch (next.getParamType()) { + case NUMBER: + sp.setType(Enumerations.SearchParamType.NUMBER); + break; + case DATE: + sp.setType(Enumerations.SearchParamType.DATE); + break; + case STRING: + sp.setType(Enumerations.SearchParamType.STRING); + break; + case TOKEN: + sp.setType(Enumerations.SearchParamType.TOKEN); + break; + case REFERENCE: + sp.setType(Enumerations.SearchParamType.REFERENCE); + break; + case COMPOSITE: + sp.setType(Enumerations.SearchParamType.COMPOSITE); + break; + case QUANTITY: + sp.setType(Enumerations.SearchParamType.QUANTITY); + break; + case URI: + sp.setType(Enumerations.SearchParamType.URI); + break; + case HAS: + case SPECIAL: + default: + return null; + } + return sp; + } + + @Nonnull + private StructureDefinition fetchStructureDefinition(String resourceName) { + StructureDefinition retVal = myContext.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + Validate.notNull(retVal); + return retVal; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 6eea53776ff..0a2dc5bc0e0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -722,16 +722,6 @@ public abstract class BaseJpaTest extends BaseTest { doRandomizeLocaleAndTimezone(); } - @AfterAll - public static void afterClassShutdownDerby() { - // DriverManager.getConnection("jdbc:derby:;shutdown=true"); - // try { - // DriverManager.getConnection("jdbc:derby:memory:myUnitTestDB;drop=true"); - // } catch (SQLNonTransientConnectionException e) { - // // expected.. for some reason.... - // } - } - public static String loadClasspath(String resource) throws IOException { return new String(loadClasspathBytes(resource), Constants.CHARSET_UTF8); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index bfd84b16f0e..10bde60f354 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -17,6 +17,7 @@ import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -105,6 +106,10 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { .collect(Collectors.toList()); assertThat(paths.toString(), paths, contains("Observation.subject", "Observation.subject.where(resolve() is Patient)")); }); + + myCaptureQueriesListener.clear(); + assertEquals(1, myObservationDao.search(SearchParameterMap.newSynchronous("patient", new ReferenceParam("Patient/A"))).sizeOrThrowNpe()); + myCaptureQueriesListener.logSelectQueries(); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java similarity index 79% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java index 7d537b6d16b..bda7ff0e3d9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java @@ -5,12 +5,9 @@ import ca.uhn.fhir.jpa.graphql.GraphQLProvider; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.IOUtils; @@ -19,9 +16,6 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -35,6 +29,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -47,15 +42,19 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; -public class JpaGraphQLR4ProviderTest { +public class GraphQLR4ProviderTest { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaGraphQLR4ProviderTest.class); public static final String DATA_PREFIX = "{\"data\": "; public static final String DATA_SUFFIX = "}"; - private static CloseableHttpClient ourClient; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLR4ProviderTest.class); private static final FhirContext ourCtx = FhirContext.forR4Cached(); - private static int ourPort; - private static Server ourServer; + private static CloseableHttpClient ourClient; + private MyStorageServices myGraphQLStorageServices = new MyStorageServices(); + + @RegisterExtension + private RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx) + .registerProvider(new DummyPatientResourceProvider()) + .registerProvider(new GraphQLProvider(myGraphQLStorageServices)); @BeforeEach public void before() { @@ -65,9 +64,8 @@ public class JpaGraphQLR4ProviderTest { @Test public void testGraphInstance() throws Exception { String query = "{name{family,given}}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -81,9 +79,6 @@ public class JpaGraphQLR4ProviderTest { " }]\n" + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); - - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); } } @@ -91,9 +86,8 @@ public class JpaGraphQLR4ProviderTest { @Test public void testGraphInstanceWithFhirpath() throws Exception { String query = "{name(fhirpath:\"family.exists()\"){text,given,family}}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -105,9 +99,6 @@ public class JpaGraphQLR4ProviderTest { " }]\n" + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); - - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); } } @@ -115,9 +106,8 @@ public class JpaGraphQLR4ProviderTest { @Test public void testGraphSystemInstance() throws Exception { String query = "{Patient(id:123){id,name{given,family}}}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -133,9 +123,6 @@ public class JpaGraphQLR4ProviderTest { " }\n" + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); - - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); } } @@ -143,9 +130,9 @@ public class JpaGraphQLR4ProviderTest { @Test public void testGraphSystemList() throws Exception { String query = "{PatientList(name:\"pet\"){name{family,given}}}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -166,8 +153,6 @@ public class JpaGraphQLR4ProviderTest { "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); } } @@ -175,9 +160,8 @@ public class JpaGraphQLR4ProviderTest { @Test public void testGraphSystemArrayArgumentList() throws Exception { String query = "{PatientList(id:[\"hapi-123\",\"hapi-124\"]){id,name{family}}}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -196,41 +180,21 @@ public class JpaGraphQLR4ProviderTest { " }]\n" + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); - - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); } } @AfterAll public static void afterClassClearContext() throws Exception { - JettyUtil.closeServer(ourServer); + ourClient.close(); } @BeforeAll public static void beforeClass() throws Exception { - ourServer = new Server(0); - - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - servlet.setDefaultResponseEncoding(EncodingEnum.JSON); - servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); - - servlet.registerProvider(new DummyPatientResourceProvider()); - MyStorageServices storageServices = new MyStorageServices(); - servlet.registerProvider(new GraphQLProvider(storageServices)); - ServletHolder servletHolder = new ServletHolder(servlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - JettyUtil.startServer(ourServer); - ourPort = JettyUtil.getPortForStartedServer(ourServer); - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); HttpClientBuilder builder = HttpClientBuilder.create(); builder.setConnectionManager(connectionManager); ourClient = builder.build(); - } public static class DummyPatientResourceProvider implements IResourceProvider { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java index bdacba5605a..f433c6ff320 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java @@ -14,8 +14,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.StandardCharsets; -import static ca.uhn.fhir.jpa.provider.JpaGraphQLR4ProviderTest.DATA_PREFIX; -import static ca.uhn.fhir.jpa.provider.JpaGraphQLR4ProviderTest.DATA_SUFFIX; +import static ca.uhn.fhir.jpa.provider.GraphQLR4ProviderTest.DATA_PREFIX; +import static ca.uhn.fhir.jpa.provider.GraphQLR4ProviderTest.DATA_SUFFIX; import static org.junit.jupiter.api.Assertions.assertEquals; public class GraphQLProviderDstu3Test extends BaseResourceProviderDstu3Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLProviderR4Test.java deleted file mode 100644 index 9b8bb600bdc..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLProviderR4Test.java +++ /dev/null @@ -1,93 +0,0 @@ -package ca.uhn.fhir.jpa.provider.r4; - -import ca.uhn.fhir.util.TestUtil; -import ca.uhn.fhir.util.UrlUtil; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import static ca.uhn.fhir.jpa.provider.JpaGraphQLR4ProviderTest.DATA_PREFIX; -import static ca.uhn.fhir.jpa.provider.JpaGraphQLR4ProviderTest.DATA_SUFFIX; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class GraphQLProviderR4Test extends BaseResourceProviderR4Test { - private Logger ourLog = LoggerFactory.getLogger(GraphQLProviderR4Test.class); - private IIdType myPatientId0; - - @Test - public void testInstanceSimpleRead() throws IOException { - initTestPatients(); - - String query = "{name{family,given}}"; - HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - - try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { - String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info(resp); - assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" + - " \"name\":[{\n" + - " \"family\":\"FAM\",\n" + - " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + - " },{\n" + - " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + - " }]\n" + - "}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp)); - } - - } - - @Test - public void testSystemSimpleSearch() throws IOException { - initTestPatients(); - - String query = "{PatientList(given:\"given\"){name{family,given}}}"; - HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - - try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { - String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info(resp); - assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" + - " \"PatientList\":[{\n" + - " \"name\":[{\n" + - " \"family\":\"FAM\",\n" + - " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + - " },{\n" + - " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + - " }]\n" + - " },{\n" + - " \"name\":[{\n" + - " \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" + - " }]\n" + - " }]\n" + - "}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp)); - } - } - - private void initTestPatients() { - Patient p = new Patient(); - p.addName() - .setFamily("FAM") - .addGiven("GIVEN1") - .addGiven("GIVEN2"); - p.addName() - .addGiven("GivenOnly1") - .addGiven("GivenOnly2"); - myPatientId0 = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addName() - .addGiven("GivenOnlyB1") - .addGiven("GivenOnlyB2"); - myClient.create().resource(p).execute(); - } - - -} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLR4Test.java new file mode 100644 index 00000000000..361eb1dd92b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/GraphQLR4Test.java @@ -0,0 +1,227 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.util.FileUtil; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static ca.uhn.fhir.jpa.provider.GraphQLR4ProviderTest.DATA_PREFIX; +import static ca.uhn.fhir.jpa.provider.GraphQLR4ProviderTest.DATA_SUFFIX; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class GraphQLR4Test extends BaseResourceProviderR4Test { + public static final String INTROSPECTION_QUERY = "{\"query\":\"\\n query IntrospectionQuery {\\n __schema {\\n queryType { name }\\n mutationType { name }\\n subscriptionType { name }\\n types {\\n ...FullType\\n }\\n directives {\\n name\\n description\\n locations\\n args {\\n ...InputValue\\n }\\n }\\n }\\n }\\n\\n fragment FullType on __Type {\\n kind\\n name\\n description\\n fields(includeDeprecated: true) {\\n name\\n description\\n args {\\n ...InputValue\\n }\\n type {\\n ...TypeRef\\n }\\n isDeprecated\\n deprecationReason\\n }\\n inputFields {\\n ...InputValue\\n }\\n interfaces {\\n ...TypeRef\\n }\\n enumValues(includeDeprecated: true) {\\n name\\n description\\n isDeprecated\\n deprecationReason\\n }\\n possibleTypes {\\n ...TypeRef\\n }\\n }\\n\\n fragment InputValue on __InputValue {\\n name\\n description\\n type { ...TypeRef }\\n defaultValue\\n }\\n\\n fragment TypeRef on __Type {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n \",\"operationName\":\"IntrospectionQuery\"}"; + private Logger ourLog = LoggerFactory.getLogger(GraphQLR4Test.class); + private IIdType myPatientId0; + + @Test + public void testInstance_Read_Patient() throws IOException { + initTestPatients(); + + String query = "{name{family,given}}"; + HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" + + " \"name\":[{\n" + + " \"family\":\"FAM\",\n" + + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + + " },{\n" + + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + + " }]\n" + + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp)); + } + + } + + @Test + public void testType_Introspect_Patient() throws IOException { + initTestPatients(); + + String uri = ourServerBase + "/Patient/$graphql"; + HttpPost httpGet = new HttpPost(uri); + httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON)); + + // Repeat a couple of times to make sure it doesn't fail after the first one. At one point + // the generator polluted the structure userdata and failed the second time + for (int i = 0; i < 3; i++) { + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\",")); + assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\","))); + assertThat(resp, not(containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\""))); + assertThat(resp, not(containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\""))); + assertThat(resp, not(containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\""))); + assertThat(resp, containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\"")); + assertThat(resp, containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\"")); + } + } + } + + @Test + public void testType_Introspect_Observation() throws IOException { + initTestPatients(); + + String uri = ourServerBase + "/Observation/$graphql"; + HttpPost httpGet = new HttpPost(uri); + httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON)); + + // Repeat a couple of times to make sure it doesn't fail after the first one. At one point + // the generator polluted the structure userdata and failed the second time + for (int i = 0; i < 3; i++) { + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\","))); + assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\",")); + assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"PatientList\""))); + assertThat(resp, containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\"")); + assertThat(resp, containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\"")); + assertThat(resp, containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\"")); + assertThat(resp, not(containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\""))); + assertThat(resp, not(containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\""))); + } + } + } + + @Test + public void testRoot_Introspect() throws IOException { + initTestPatients(); + + String uri = ourServerBase + "/$graphql"; + HttpPost httpGet = new HttpPost(uri); + httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON)); + + // Repeat a couple of times to make sure it doesn't fail after the first one. At one point + // the generator polluted the structure userdata and failed the second time + for (int i = 0; i < 3; i++) { + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response has size: {}", FileUtil.formatFileSize(resp.length())); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\",")); + assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\",")); + assertThat(resp, containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\"")); + assertThat(resp, containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\"")); + assertThat(resp, containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\"")); + assertThat(resp, containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\"")); + assertThat(resp, containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\"")); + } + } + } + + @Test + public void testRoot_Read_Patient() throws IOException { + initTestPatients(); + + String query = "{Patient(id:\"" + myPatientId0.getIdPart() + "\"){name{family,given}}}"; + HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + + "{\n" + + "\"Patient\":{\n" + + "\"name\":[{\n" + + "\"family\":\"FAM\",\n" + + "\"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + + "},{\n" + + "\"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + + "}]\n" + + "}\n" + + "}" + + DATA_SUFFIX), TestUtil.stripWhitespace(resp)); + } + + } + + + + @Test + public void testRoot_Search_Patient() throws IOException { + initTestPatients(); + + String query = "{PatientList(given:\"given\"){name{family,given}}}"; + HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" + + " \"PatientList\":[{\n" + + " \"name\":[{\n" + + " \"family\":\"FAM\",\n" + + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + + " },{\n" + + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + + " }]\n" + + " },{\n" + + " \"name\":[{\n" + + " \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" + + " }]\n" + + " }]\n" + + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp)); + } + + } + + @Test + public void testRoot_Search_Observation() throws IOException { + initTestPatients(); + + String query = "{ObservationList(date: \"2022\") {id}}"; + HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + + myCaptureQueriesListener.clear(); + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + } + myCaptureQueriesListener.logSelectQueries(); + } + + private void initTestPatients() { + Patient p = new Patient(); + p.addName() + .setFamily("FAM") + .addGiven("GIVEN1") + .addGiven("GIVEN2"); + p.addName() + .addGiven("GivenOnly1") + .addGiven("GivenOnly2"); + myPatientId0 = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addName() + .addGiven("GivenOnlyB1") + .addGiven("GivenOnlyB2"); + myClient.create().resource(p).execute(); + } + + +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 61611c4861e..2d45a2653bc 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -19,6 +19,7 @@ org.postgresql postgresql + ca.uhn.hapi.fhir hapi-fhir-jpaserver-base @@ -102,6 +103,12 @@ + + com.h2database + h2 + test + + org.eclipse.jetty jetty-servlets diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index 2c1625f16fd..4672525292b 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.server.interceptor.FhirPathFilterInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; +import ca.uhn.fhirtest.config.SqlCaptureInterceptor; import ca.uhn.fhirtest.config.TestDstu2Config; import ca.uhn.fhirtest.config.TestDstu3Config; import ca.uhn.fhirtest.config.TestR4Config; @@ -282,6 +283,8 @@ public class TestRestfulServer extends RestfulServer { loggingInterceptor.setMessageFormat("${operationType} Content-Type: ${requestHeader.content-type} - Accept: ${responseEncodingNoDefault} \"${requestHeader.accept}\" - Agent: ${requestHeader.user-agent}"); registerInterceptor(loggingInterceptor); + // SQL Capturing + registerInterceptor(myAppCtx.getBean(SqlCaptureInterceptor.class)); } /** diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index 4f5d2c8a0ec..e187f6aa4c8 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -5,11 +5,11 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.JpaDstu2Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; -import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.IValidatorModule; @@ -22,7 +22,6 @@ import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,16 +43,11 @@ import java.util.concurrent.TimeUnit; @EnableTransactionManagement() public class TestDstu2Config { - public static final String FHIR_LUCENE_LOCATION_DSTU2 = "${fhir.lucene.location.dstu2}"; + public static final String FHIR_LUCENE_LOCATION_DSTU2 = "fhir.lucene.location.dstu2"; - @Value(TestDstu3Config.FHIR_DB_USERNAME) - private String myDbUsername; - - @Value(TestDstu3Config.FHIR_DB_PASSWORD) - private String myDbPassword; - - @Value(FHIR_LUCENE_LOCATION_DSTU2) - private String myFhirLuceneLocation; + private String myDbUsername = System.getProperty(TestR5Config.FHIR_DB_USERNAME); + private String myDbPassword = System.getProperty(TestR5Config.FHIR_DB_PASSWORD); + private String myFhirLuceneLocation = System.getProperty(FHIR_LUCENE_LOCATION_DSTU2); @Bean public PublicSecurityInterceptor securityInterceptor() { @@ -88,7 +82,9 @@ public class TestDstu2Config { @Bean public ModelConfig modelConfig() { - return daoConfig().getModelConfig(); + ModelConfig retVal = daoConfig().getModelConfig(); + retVal.setIndexIdentifierOfType(true); + return retVal; } @Bean @@ -103,7 +99,7 @@ public class TestDstu2Config { public DataSource dataSource() { BasicDataSource retVal = new BasicDataSource(); if (CommonConfig.isLocalTestMode()) { - retVal.setUrl("jdbc:derby:memory:fhirtest_dstu2;create=true"); + retVal.setUrl("jdbc:h2:mem:fhirtest_dstu2"); } else { retVal.setDriver(new org.postgresql.Driver()); retVal.setUrl("jdbc:postgresql://localhost/fhirtest_dstu2"); @@ -144,7 +140,7 @@ public class TestDstu2Config { private Properties jpaProperties() { Properties extraProperties = new Properties(); if (CommonConfig.isLocalTestMode()) { - extraProperties.put("hibernate.dialect", DerbyTenSevenHapiFhirDialect.class.getName()); + extraProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName()); } else { extraProperties.put("hibernate.dialect", HapiFhirPostgres94Dialect.class.getName()); } @@ -168,6 +164,7 @@ public class TestDstu2Config { /** * Bean which validates incoming requests + * * @param theInstanceValidator */ @Bean diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index 21e07571248..6ba5411c541 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -5,12 +5,12 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.dstu3.JpaDstu3Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; -import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.IInstanceValidatorModule; @@ -23,7 +23,6 @@ import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,17 +44,12 @@ import java.util.concurrent.TimeUnit; @EnableTransactionManagement() public class TestDstu3Config { public static final String FHIR_DB_USERNAME = "${fhir.db.username}"; - public static final String FHIR_DB_PASSWORD = "${fhir.db.password}"; + public static final String FHIR_DB_PASSWORD = "${fhir.db.password}"; public static final String FHIR_LUCENE_LOCATION_DSTU3 = "${fhir.lucene.location.dstu3}"; - @Value(TestDstu3Config.FHIR_DB_USERNAME) - private String myDbUsername; - - @Value(TestDstu3Config.FHIR_DB_PASSWORD) - private String myDbPassword; - - @Value(FHIR_LUCENE_LOCATION_DSTU3) - private String myFhirLuceneLocation; + private String myDbUsername = System.getProperty(TestR5Config.FHIR_DB_USERNAME); + private String myDbPassword = System.getProperty(TestR5Config.FHIR_DB_PASSWORD); + private String myFhirLuceneLocation = System.getProperty(FHIR_LUCENE_LOCATION_DSTU3); @Bean public DaoConfig daoConfig() { @@ -85,7 +79,9 @@ public class TestDstu3Config { @Bean public ModelConfig modelConfig() { - return daoConfig().getModelConfig(); + ModelConfig retVal = daoConfig().getModelConfig(); + retVal.setIndexIdentifierOfType(true); + return retVal; } @@ -104,8 +100,8 @@ public class TestDstu3Config { return retVal; } - - @Bean + + @Bean public PublicSecurityInterceptor securityInterceptor() { return new PublicSecurityInterceptor(); } @@ -114,7 +110,7 @@ public class TestDstu3Config { public DataSource dataSource() { BasicDataSource retVal = new BasicDataSource(); if (CommonConfig.isLocalTestMode()) { - retVal.setUrl("jdbc:derby:memory:fhirtest_dstu3;create=true"); + retVal.setUrl("jdbc:h2:mem:fhirtest_dstu3"); } else { retVal.setDriver(new org.postgresql.Driver()); retVal.setUrl("jdbc:postgresql://localhost/fhirtest_dstu3"); @@ -147,7 +143,7 @@ public class TestDstu3Config { private Properties jpaProperties() { Properties extraProperties = new Properties(); if (CommonConfig.isLocalTestMode()) { - extraProperties.put("hibernate.dialect", DerbyTenSevenHapiFhirDialect.class.getName()); + extraProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName()); } else { extraProperties.put("hibernate.dialect", HapiFhirPostgres94Dialect.class.getName()); } @@ -171,6 +167,7 @@ public class TestDstu3Config { /** * Bean which validates incoming requests + * * @param theFhirInstanceValidator */ @Bean diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index 8e30629dc29..427118fc223 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -5,12 +5,12 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.r4.JpaR4Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; -import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.IInstanceValidatorModule; @@ -23,7 +23,6 @@ import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,14 +48,9 @@ public class TestR4Config { public static final String FHIR_LUCENE_LOCATION_R4 = "${fhir.lucene.location.r4}"; public static final Integer COUNT_SEARCH_RESULTS_UP_TO = 50000; - @Value(TestR4Config.FHIR_DB_USERNAME) - private String myDbUsername; - - @Value(TestR4Config.FHIR_DB_PASSWORD) - private String myDbPassword; - - @Value(FHIR_LUCENE_LOCATION_R4) - private String myFhirLuceneLocation; + private String myDbUsername = System.getProperty(TestR5Config.FHIR_DB_USERNAME); + private String myDbPassword = System.getProperty(TestR5Config.FHIR_DB_PASSWORD); + private String myFhirLuceneLocation = System.getProperty(FHIR_LUCENE_LOCATION_R4); @Bean public DaoConfig daoConfig() { @@ -85,7 +79,9 @@ public class TestR4Config { @Bean public ModelConfig modelConfig() { - return daoConfig().getModelConfig(); + ModelConfig retVal = daoConfig().getModelConfig(); + retVal.setIndexIdentifierOfType(true); + return retVal; } @@ -101,7 +97,7 @@ public class TestR4Config { public DataSource dataSource() { BasicDataSource retVal = new BasicDataSource(); if (CommonConfig.isLocalTestMode()) { - retVal.setUrl("jdbc:derby:memory:fhirtest_r4;create=true"); + retVal.setUrl("jdbc:h2:mem:fhirtest_r4"); } else { retVal.setDriver(new org.postgresql.Driver()); retVal.setUrl("jdbc:postgresql://localhost/fhirtest_r4"); @@ -142,7 +138,7 @@ public class TestR4Config { private Properties jpaProperties() { Properties extraProperties = new Properties(); if (CommonConfig.isLocalTestMode()) { - extraProperties.put("hibernate.dialect", DerbyTenSevenHapiFhirDialect.class.getName()); + extraProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName()); } else { extraProperties.put("hibernate.dialect", HapiFhirPostgres94Dialect.class.getName()); } @@ -165,6 +161,7 @@ public class TestR4Config { /** * Bean which validates incoming requests + * * @param theFhirInstanceValidator */ @Bean @@ -202,5 +199,4 @@ public class TestR4Config { } - } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java index 9b668d7d0a7..c55c2a074f9 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java @@ -5,12 +5,12 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.r5.JpaR5Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; -import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.IInstanceValidatorModule; @@ -23,7 +23,8 @@ import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; import org.hibernate.search.engine.cfg.BackendSettings; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.springframework.beans.factory.annotation.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,15 +49,10 @@ public class TestR5Config { public static final String FHIR_DB_PASSWORD = "${fhir.db.password}"; public static final String FHIR_LUCENE_LOCATION_R5 = "${fhir.lucene.location.r5}"; public static final Integer COUNT_SEARCH_RESULTS_UP_TO = 50000; - - @Value(TestR5Config.FHIR_DB_USERNAME) - private String myDbUsername; - - @Value(TestR5Config.FHIR_DB_PASSWORD) - private String myDbPassword; - - @Value(FHIR_LUCENE_LOCATION_R5) - private String myFhirLuceneLocation; + private static final Logger ourLog = LoggerFactory.getLogger(TestR5Config.class); + private String myDbUsername = System.getProperty(TestR5Config.FHIR_DB_USERNAME); + private String myDbPassword = System.getProperty(TestR5Config.FHIR_DB_PASSWORD); + private String myFhirLuceneLocation = System.getProperty(FHIR_LUCENE_LOCATION_R5); @Bean public DaoConfig daoConfig() { @@ -85,7 +81,9 @@ public class TestR5Config { @Bean public ModelConfig modelConfig() { - return daoConfig().getModelConfig(); + ModelConfig retVal = daoConfig().getModelConfig(); + retVal.setIndexIdentifierOfType(true); + return retVal; } @Bean @@ -97,9 +95,12 @@ public class TestR5Config { @Bean(name = "myPersistenceDataSourceR5") public DataSource dataSource() { + ourLog.info("Starting R5 database with DB username: {}", myDbUsername); + ourLog.info("Have system property username: {}", System.getProperty(FHIR_DB_USERNAME)); + BasicDataSource retVal = new BasicDataSource(); if (CommonConfig.isLocalTestMode()) { - retVal.setUrl("jdbc:derby:memory:fhirtest_r5;create=true"); + retVal.setUrl("jdbc:h2:mem:fhirtest_r5"); } else { retVal.setDriver(new org.postgresql.Driver()); retVal.setUrl("jdbc:postgresql://localhost/fhirtest_r5"); @@ -141,7 +142,7 @@ public class TestR5Config { private Properties jpaProperties() { Properties extraProperties = new Properties(); if (CommonConfig.isLocalTestMode()) { - extraProperties.put("hibernate.dialect", DerbyTenSevenHapiFhirDialect.class.getName()); + extraProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName()); } else { extraProperties.put("hibernate.dialect", HapiFhirPostgres94Dialect.class.getName()); } @@ -165,6 +166,7 @@ public class TestR5Config { /** * Bean which validates incoming requests + * * @param theFhirInstanceValidator */ @Bean @@ -202,5 +204,4 @@ public class TestR5Config { } - } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProvider.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProvider.java index 9fd2288183a..3ee76f0dd04 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProvider.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/GraphQLProvider.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.GraphQL; @@ -32,6 +33,7 @@ import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -39,12 +41,12 @@ import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; -import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.utilities.graphql.IGraphQLEngine; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.graphql.ObjectValue; +import org.hl7.fhir.utilities.graphql.Package; import org.hl7.fhir.utilities.graphql.Parser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,9 +56,10 @@ import javax.annotation.Nullable; import java.util.function.Supplier; public class GraphQLProvider { - private final Supplier engineFactory; + private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); + + private final Supplier myEngineFactory; private final IGraphQLStorageServices myStorageServices; - private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); /** * Constructor which uses a default context and validation support object @@ -83,21 +86,21 @@ public class GraphQLProvider { IValidationSupport validationSupport = theValidationSupport; validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); - engineFactory = () -> new org.hl7.fhir.dstu3.utils.GraphQLEngine(workerContext); + myEngineFactory = () -> new org.hl7.fhir.dstu3.utils.GraphQLEngine(workerContext); break; } case R4: { IValidationSupport validationSupport = theValidationSupport; validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); - engineFactory = () -> new org.hl7.fhir.r4.utils.GraphQLEngine(workerContext); + myEngineFactory = () -> new org.hl7.fhir.r4.utils.GraphQLEngine(workerContext); break; } case R5: { IValidationSupport validationSupport = theValidationSupport; validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); - engineFactory = () -> new org.hl7.fhir.r5.utils.GraphQLEngine(workerContext); + myEngineFactory = () -> new org.hl7.fhir.r5.utils.GraphQLEngine(workerContext); break; } case DSTU2: @@ -111,8 +114,8 @@ public class GraphQLProvider { myStorageServices = theStorageServices; } - @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") - @GraphQL(type=RequestTypeEnum.GET) + @Description(value = "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") + @GraphQL(type = RequestTypeEnum.GET) public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQueryUrl) { if (theQueryUrl != null) { return processGraphQLRequest(theRequestDetails, theId, theQueryUrl); @@ -120,24 +123,31 @@ public class GraphQLProvider { throw new InvalidRequestException(Msg.code(1144) + "Unable to parse empty GraphQL expression"); } - @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") - @GraphQL(type=RequestTypeEnum.POST) - public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String theQueryBody) { + @Description(value = "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") + @GraphQL(type = RequestTypeEnum.POST) + public String processGraphQlPostRequest(ServletRequestDetails theServletRequestDetails, RequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String theQueryBody) { if (theQueryBody != null) { - return processGraphQLRequest(theRequestDetails, theId, theQueryBody); + return processGraphQLRequest(theServletRequestDetails, theId, theQueryBody); } throw new InvalidRequestException(Msg.code(1145) + "Unable to parse empty GraphQL expression"); } public String processGraphQLRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQuery) { - IGraphQLEngine engine = engineFactory.get(); + Package parsedGraphQLRequest; + try { + parsedGraphQLRequest = Parser.parse(theQuery); + } catch (Exception e) { + throw new InvalidRequestException(Msg.code(1146) + "Unable to parse GraphQL Expression: " + e); + } + + return processGraphQLRequest(theRequestDetails, theId, parsedGraphQLRequest); + } + + protected String processGraphQLRequest(ServletRequestDetails theRequestDetails, IIdType theId, Package parsedGraphQLRequest) { + IGraphQLEngine engine = myEngineFactory.get(); engine.setAppInfo(theRequestDetails); engine.setServices(myStorageServices); - try { - engine.setGraphQL(Parser.parse(theQuery)); - } catch (Exception theE) { - throw new InvalidRequestException(Msg.code(1146) + "Unable to parse GraphQL Expression: " + theE.toString()); - } + engine.setGraphQL(parsedGraphQLRequest); try { @@ -167,7 +177,7 @@ public class GraphQLProvider { ourLog.error("Failure during GraphQL processing", e); } b.append(e.getMessage()); - throw new UnclassifiedServerFailureException(statusCode, Msg.code(1147) + b.toString()); + throw new UnclassifiedServerFailureException(statusCode, Msg.code(1147) + b); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java index 389cd2d9b2c..2f4109fb05e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java @@ -41,7 +41,7 @@ import static org.mockito.Mockito.when; public class LoggingInterceptorTest { - private static FhirContext ourCtx = FhirContext.forR4(); + private static final FhirContext ourCtx = FhirContext.forR4Cached(); private static int ourPort; private static Server ourServer; private Logger myLoggerRoot; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java index 2b1a827b3bc..44a36e49578 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java @@ -8,10 +8,10 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.IOUtils; @@ -22,9 +22,6 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.IdType; @@ -33,10 +30,10 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -44,63 +41,37 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class GraphQLR4RawTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLR4RawTest.class); private static CloseableHttpClient ourClient; - private static FhirContext ourCtx = FhirContext.forR4(); - private static int ourPort; - private static Server ourServer; + private static final FhirContext ourCtx = FhirContext.forR4Cached(); private static String ourNextRetVal; private static IdType ourLastId; private static String ourLastQuery; - private static int ourMethodCount; + private static String ourLastResourceType; - @AfterAll - public static void afterClassClearContext() throws Exception { - JettyUtil.closeServer(ourServer); - TestUtil.randomizeLocaleAndTimezone(); - } - - @BeforeAll - public static void beforeClass() throws Exception { - ourServer = new Server(0); - - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - servlet.setDefaultResponseEncoding(EncodingEnum.JSON); - servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); - - servlet.registerProviders(Collections.singletonList(new MyGraphQLProvider())); - servlet.registerProvider(new MyPatientResourceProvider()); - ServletHolder servletHolder = new ServletHolder(servlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - JettyUtil.startServer(ourServer); - ourPort = JettyUtil.getPortForStartedServer(ourServer); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); - - } + @RegisterExtension + private final RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx) + .registerProvider(new MyPatientResourceProvider()) + .registerProvider(new MyGraphQLProvider()); @BeforeEach public void before() { ourNextRetVal = null; ourLastId = null; ourLastQuery = null; - ourMethodCount = 0; + ourLastResourceType = null; } @Test - public void testGraphInstance() throws Exception { + public void testGraphInstance_Get() throws Exception { ourNextRetVal = "{\"foo\"}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/Patient/123/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); CloseableHttpResponse status = ourClient.execute(httpGet); try { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); @@ -118,12 +89,29 @@ public class GraphQLR4RawTest { } - @Test - public void testGraphPostContentTypeJson() throws Exception { + public void testGraphInstance_Get_UnsupportedResourceType() throws Exception { ourNextRetVal = "{\"foo\"}"; - HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$graphql"); + + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/Condition/123/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); + CloseableHttpResponse status = ourClient.execute(httpGet); + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(responseContent, containsString("Unknown resource type")); + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @Test + public void testGraphInstance_Post_ContentTypeJson() throws Exception { + ourNextRetVal = "{\"foo\"}"; + + HttpPost httpPost = new HttpPost("http://localhost:" + myRestfulServerExtension.getPort() + "/Patient/123/$graphql"); StringEntity entity = new StringEntity("{\"query\": \"{name{family,given}}\"}"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); @@ -147,10 +135,10 @@ public class GraphQLR4RawTest { } @Test - public void testGraphPostContentTypeGraphql() throws Exception { + public void testGraphInstance_Post_ContentTypeGraphql() throws Exception { ourNextRetVal = "{\"foo\"}"; - HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$graphql"); + HttpPost httpPost = new HttpPost("http://localhost:" + myRestfulServerExtension.getPort() + "/Patient/123/$graphql"); StringEntity entity = new StringEntity("{name{family,given}}"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); @@ -166,6 +154,7 @@ public class GraphQLR4RawTest { assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); assertEquals("Patient/123", ourLastId.getValue()); assertEquals("{name{family,given}}", ourLastQuery); + assertEquals("Patient", ourLastResourceType); } finally { IOUtils.closeQuietly(status.getEntity().getContent()); @@ -173,31 +162,41 @@ public class GraphQLR4RawTest { } - @Test - public void testGraphInstanceUnknownType() throws Exception { + public void testGraphBase_Post_ListQuery() throws Exception { ourNextRetVal = "{\"foo\"}"; + HttpPost httpPost = new HttpPost("http://localhost:" + myRestfulServerExtension.getPort() + "/$graphql"); + StringEntity entity = new StringEntity("{\"query\": \"{PatientList(date: \\\"2022\\\") {name{family,given}}}\"}"); + httpPost.setEntity(entity); + httpPost.setHeader("Accept", "application/json"); + httpPost.setHeader("Content-type", "application/json"); - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Condition/123/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); - CloseableHttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpPost); try { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); - assertEquals(404, status.getStatusLine().getStatusCode()); - assertThat(responseContent, containsString("Unknown resource type")); + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertEquals("{\"foo\"}", responseContent); + assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); + assertNull(ourLastId); + assertNull(ourLastResourceType); + assertEquals("{PatientList(date: \"2022\") {name{family,given}}}", ourLastQuery); + } finally { IOUtils.closeQuietly(status.getEntity().getContent()); } } + @Test public void testGraphSystem() throws Exception { ourNextRetVal = "{\"foo\"}"; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); + HttpGet httpGet = new HttpGet("http://localhost:" + myRestfulServerExtension.getPort() + "/$graphql?query=" + UrlUtil.escapeUrlParam("{name{family,given}}")); CloseableHttpResponse status = ourClient.execute(httpGet); try { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); @@ -215,20 +214,33 @@ public class GraphQLR4RawTest { } + @AfterAll + public static void afterClassClearContext() throws Exception { + ourClient.close(); + TestUtil.randomizeLocaleAndTimezone(); + } + + @BeforeAll + public static void beforeClass() throws Exception { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + } + public static class MyGraphQLProvider { - @GraphQL(type=RequestTypeEnum.GET) + @GraphQL(type = RequestTypeEnum.GET) public String processGet(@IdParam IdType theId, @GraphQLQueryUrl String theQuery) { - ourMethodCount++; ourLastId = theId; ourLastQuery = theQuery; return ourNextRetVal; } - @GraphQL(type=RequestTypeEnum.POST) - public String processPost(@IdParam IdType theId, @GraphQLQueryBody String theQuery) { - ourMethodCount++; + @GraphQL(type = RequestTypeEnum.POST) + public String processPost(RequestDetails theRequestDetails, @IdParam IdType theId, @GraphQLQueryBody String theQuery) { ourLastId = theId; + ourLastResourceType = theRequestDetails.getResourceName(); ourLastQuery = theQuery; return ourNextRetVal; } @@ -246,13 +258,13 @@ public class GraphQLR4RawTest { @Search() public List search( @OptionalParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) { - ArrayList retVal = new ArrayList(); + ArrayList retVal = new ArrayList<>(); for (int i = 0; i < 200; i++) { Patient patient = new Patient(); patient.addName(new HumanName().setFamily("FAMILY")); patient.getIdElement().setValue("Patient/" + i); - retVal.add((Patient) patient); + retVal.add(patient); } return retVal; } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/GraphQLEngineTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/GraphQLEngineTest.java index 9ce33002469..86fad91f301 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/GraphQLEngineTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/GraphQLEngineTest.java @@ -3,8 +3,10 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Period; import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; @@ -88,6 +90,53 @@ public class GraphQLEngineTest { } + + @Test + public void testChoiceType_SelectDifferentType() throws EGraphEngine, EGraphQLException, IOException { + Observation obs = new Observation(); + obs.setId("http://foo.com/Patient/PATA"); + obs.setEffective(new Period().setStartElement(new DateTimeType("2022-01-01T00:00:00Z")).setEndElement(new DateTimeType("2022-01-01T05:00:00Z"))); + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(obs); + engine.setGraphQL(Parser.parse("{id, effectiveDateTime}")); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"id\":\"http://foo.com/Patient/PATA\"\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + } + + @Test + public void testChoiceType_SelectSameType() throws EGraphEngine, EGraphQLException, IOException { + Observation obs = new Observation(); + obs.setId("http://foo.com/Patient/PATA"); + obs.setEffective(new DateTimeType("2022-01-01T12:12:12Z")); + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(obs); + engine.setGraphQL(Parser.parse("{id, effectiveDateTime}")); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"id\":\"http://foo.com/Patient/PATA\",\n" + + " \"effectiveDateTime\":\"2022-01-01T12:12:12Z\"\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + } + + @Test public void testReferences() throws EGraphQLException, EGraphEngine, IOException, FHIRException { @@ -128,7 +177,7 @@ public class GraphQLEngineTest { @BeforeAll public static void beforeClass() { - ourCtx = FhirContext.forR4(); + ourCtx = FhirContext.forR4Cached(); ourWorkerCtx = new HapiWorkerContext(ourCtx, ourCtx.getValidationSupport()); } diff --git a/hapi-fhir-structures-r5/src/test/java/org/hl7/fhir/r5/utils/GraphQLEngineTest.java b/hapi-fhir-structures-r5/src/test/java/org/hl7/fhir/r5/utils/GraphQLEngineTest.java new file mode 100644 index 00000000000..df6918a99ab --- /dev/null +++ b/hapi-fhir-structures-r5/src/test/java/org/hl7/fhir/r5/utils/GraphQLEngineTest.java @@ -0,0 +1,184 @@ +package org.hl7.fhir.r5.utils; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.r5.model.DateTimeType; +import org.hl7.fhir.r5.model.Observation; +import org.hl7.fhir.r5.model.Patient; +import org.hl7.fhir.r5.model.Period; +import org.hl7.fhir.r5.model.Quantity; +import org.hl7.fhir.r5.model.Reference; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.utilities.graphql.EGraphEngine; +import org.hl7.fhir.utilities.graphql.EGraphQLException; +import org.hl7.fhir.utilities.graphql.GraphQLResponse; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; +import org.hl7.fhir.utilities.graphql.Parser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GraphQLEngineTest { + private static HapiWorkerContext ourWorkerCtx; + private static FhirContext ourCtx; + private org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLEngineTest.class); + + private Observation createObservation() { + Observation obs = new Observation(); + obs.setId("http://foo.com/Patient/PATA"); + obs.setValue(new Quantity().setValue(123).setUnit("cm")); + obs.setSubject(new Reference("Patient/123")); + return obs; + } + + private IGraphQLStorageServices createStorageServices() throws FHIRException { + IGraphQLStorageServices retVal = mock(IGraphQLStorageServices.class); + when(retVal.lookup(nullable(Object.class), nullable(Resource.class), nullable(Reference.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + Object appInfo = invocation.getArguments()[0]; + Resource context = (Resource) invocation.getArguments()[1]; + Reference reference = (Reference) invocation.getArguments()[2]; + ourLog.info("AppInfo: {} / Context: {} / Reference: {}", appInfo, context.getId(), reference.getReference()); + + if (reference.getReference().equalsIgnoreCase("Patient/123")) { + Patient p = new Patient(); + p.getBirthDateElement().setValueAsString("2011-02-22"); + return new IGraphQLStorageServices.ReferenceResolution(context, p); + } + + ourLog.info("Not found!"); + return null; + } + }); + + return retVal; + } + + @Test + public void testGraphSimple() throws EGraphQLException, EGraphEngine, IOException, FHIRException { + + Observation obs = createObservation(); + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(obs); + engine.setGraphQL(Parser.parse("{valueQuantity{value,unit}}")); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"valueQuantity\":{\n" + + " \"value\":123,\n" + + " \"unit\":\"cm\"\n" + + " }\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + + } + + + @Test + public void testChoiceType_SelectDifferentType() throws EGraphEngine, EGraphQLException, IOException { + Observation obs = new Observation(); + obs.setId("http://foo.com/Patient/PATA"); + obs.setEffective(new Period().setStartElement(new DateTimeType("2022-01-01T00:00:00Z")).setEndElement(new DateTimeType("2022-01-01T05:00:00Z"))); + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(obs); + engine.setGraphQL(Parser.parse("{id, effectiveDateTime}")); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"id\":\"http://foo.com/Patient/PATA\"\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + } + + @Test + public void testChoiceType_SelectSameType() throws EGraphEngine, EGraphQLException, IOException { + Observation obs = new Observation(); + obs.setId("http://foo.com/Patient/PATA"); + obs.setEffective(new DateTimeType("2022-01-01T12:12:12Z")); + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(obs); + engine.setGraphQL(Parser.parse("{id, effectiveDateTime}")); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"id\":\"http://foo.com/Patient/PATA\",\n" + + " \"effectiveDateTime\":\"2022-01-01T12:12:12Z\"\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + } + + + @Test + public void testReferences() throws EGraphQLException, EGraphEngine, IOException, FHIRException { + + String graph = " { \n" + + " id\n" + + " subject { \n" + + " reference\n" + + " resource(type : Patient) { birthDate }\n" + + " resource(type : Practioner) { practitionerRole { speciality } }\n" + + " } \n" + + " code {coding {system code} }\n" + + " }\n" + + " "; + + GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx); + engine.setFocus(createObservation()); + engine.setGraphQL(Parser.parse(graph)); + engine.setServices(createStorageServices()); + engine.execute(); + + GraphQLResponse output = engine.getOutput(); + output.setWriteWrapper(false); + StringBuilder outputBuilder = new StringBuilder(); + output.write(outputBuilder, 0, "\n"); + + String expected = "{\n" + + " \"id\":\"http://foo.com/Patient/PATA\",\n" + + " \"subject\":{\n" + + " \"reference\":\"Patient/123\",\n" + + " \"resource\":{\n" + + " \"birthDate\":\"2011-02-22\"\n" + + " }\n" + + " }\n" + + "}"; + assertEquals(TestUtil.stripReturns(expected), TestUtil.stripReturns(outputBuilder.toString())); + + } + + @BeforeAll + public static void beforeClass() { + ourCtx = FhirContext.forR5Cached(); + ourWorkerCtx = new HapiWorkerContext(ourCtx, ourCtx.getValidationSupport()); + } + +} diff --git a/pom.xml b/pom.xml index 3cb92080983..87caaf93100 100644 --- a/pom.xml +++ b/pom.xml @@ -45,15 +45,6 @@ - oss-snapshot https://oss.sonatype.org/content/repositories/snapshots/ @@ -765,7 +756,7 @@ - 5.6.27 + 5.6.35 1.0.3 -Dfile.encoding=UTF-8 -Xmx2048m @@ -924,6 +915,11 @@ caffeine ${caffeine_version} + + com.graphql-java + graphql-java + 17.3 + org.simplejavamail