From 0c9e5ec1ea49e8df895428702608d411b5d6cd8a Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 12 Aug 2019 08:24:32 -0400 Subject: [PATCH] Support GraphQL for R3/4/5 (#1424) * Work on grpahql enhanbcements * Add some more chars to the sanitizer function * Add changelog --- .../main/java/ca/uhn/fhir/util/UrlUtil.java | 40 ++++- .../java/ca/uhn/fhir/util/UrlUtilTest.java | 11 ++ .../ca/uhn/fhir/jpa/demo/JpaServerDemo.java | 2 - .../ca/uhn/fhir/jpa/config/BaseConfig.java | 8 + .../jpa/config/dstu3/BaseDstu3Config.java | 7 + .../uhn/fhir/jpa/config/r4/BaseR4Config.java | 9 +- .../uhn/fhir/jpa/config/r5/BaseR5Config.java | 20 +-- .../fhir/jpa/graphql/JpaStorageServices.java | 39 ++--- .../fhir/jpa/provider/GraphQLProvider.java | 165 ++++++++++++++++++ .../jpa/provider}/GraphQLR4ProviderTest.java | 28 +-- .../dstu3/BaseResourceProviderDstu3Test.java | 3 + .../dstu3/GraphQLProviderDstu3Test.java | 92 ++++++++++ .../r4/BaseResourceProviderR4Test.java | 6 +- .../provider/r4/GraphQLProviderR4Test.java | 15 +- .../r5/BaseResourceProviderR5Test.java | 4 +- .../ca/uhn/fhirtest/TestRestfulServer.java | 5 +- hapi-fhir-server/pom.xml | 5 + .../r4/hapi/rest/server/GraphQLProvider.java | 107 ------------ .../ca/uhn/fhir/util/GraphQLEngineTest.java | 7 +- .../r5/hapi/rest/server/GraphQLProvider.java | 107 ------------ src/changes/changes.xml | 19 +- 21 files changed, 401 insertions(+), 298 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java rename {hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server => hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider}/GraphQLR4ProviderTest.java (90%) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java delete mode 100644 hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/GraphQLProvider.java delete mode 100644 hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/GraphQLProvider.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index 727d27793b4..765876b176c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -163,7 +163,16 @@ public class UrlUtil { if (theString != null) { for (int i = 0; i < theString.length(); i++) { char nextChar = theString.charAt(i); - if (nextChar == '<' || nextChar == '"') { + switch (nextChar) { + case '\'': + case '"': + case '<': + case '>': + case '\n': + case '\r': + return true; + } + if (nextChar < ' ') { return true; } } @@ -348,7 +357,17 @@ public class UrlUtil { /** * This method specifically HTML-encodes the " and - * < characters in order to prevent injection attacks + * < characters in order to prevent injection attacks. + * + * The following characters are escaped: + * + * */ public static String sanitizeUrlPart(CharSequence theString) { if (theString == null) { @@ -364,6 +383,10 @@ public class UrlUtil { char nextChar = theString.charAt(j); switch (nextChar) { + /* + * NB: If you add a constant here, you also need to add it + * to isNeedsSanitization()!! + */ case '\'': buffer.append("'"); break; @@ -373,8 +396,19 @@ public class UrlUtil { case '<': buffer.append("<"); break; + case '>': + buffer.append(">"); + break; + case '\n': + buffer.append(" "); + break; + case '\r': + buffer.append(" "); + break; default: - buffer.append(nextChar); + if (nextChar >= ' ') { + buffer.append(nextChar); + } break; } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/UrlUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/UrlUtilTest.java index e949a6275dd..0083ba8e09f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/UrlUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/UrlUtilTest.java @@ -59,4 +59,15 @@ public class UrlUtilTest { } + @Test + public void testSanitize() { + assertEquals(" ' ", UrlUtil.sanitizeUrlPart(" ' ")); + assertEquals(" < ", UrlUtil.sanitizeUrlPart(" < ")); + assertEquals(" > ", UrlUtil.sanitizeUrlPart(" > ")); + assertEquals(" " ", UrlUtil.sanitizeUrlPart(" \" ")); + assertEquals(" ", UrlUtil.sanitizeUrlPart(" \n ")); + assertEquals(" ", UrlUtil.sanitizeUrlPart(" \r ")); + assertEquals(" ", UrlUtil.sanitizeUrlPart(" \0 ")); + } + } diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 44c970bd487..e49186f6f1e 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -24,10 +24,8 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.server.ETagSupportEnum; 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.rest.server.interceptor.CorsInterceptor; -import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index cbc9167b921..0f9b12a8d94 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; @@ -23,6 +24,7 @@ import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; @@ -109,6 +111,12 @@ public abstract class BaseConfig implements SchedulingConfigurer { public abstract FhirContext fhirContext(); + @Bean + @Lazy + public IGraphQLStorageServices graphqlStorageServices() { + return new JpaStorageServices(); + } + @Bean public ScheduledExecutorFactoryBean scheduledExecutorService() { ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index 33c1ac4c052..1f446d1d52f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; @@ -73,6 +74,12 @@ public class BaseDstu3Config extends BaseConfig { return retVal; } + @Bean(name = GRAPHQL_PROVIDER_NAME) + @Lazy + public GraphQLProvider graphQLProvider() { + return new GraphQLProvider(fhirContextDstu3(), validationSupportChainDstu3(), graphqlStorageServices()); + } + @Bean public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() { return new TransactionProcessorVersionAdapterDstu3(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index d31be7ec3b3..2bb9f1e81d3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -21,12 +21,13 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR4; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; -import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.utils.GraphQLEngine; import org.hl7.fhir.r5.utils.IResourceValidator; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -91,12 +92,6 @@ public class BaseR4Config extends BaseConfig { return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), graphqlStorageServices()); } - @Bean - @Lazy - public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() { - return new JpaStorageServices(); - } - @Bean(name = "myInstanceValidatorR4") @Lazy public IValidatorModule instanceValidatorR4() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java index 7d248657283..0edd674017f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java @@ -8,7 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.r5.TransactionProcessorVersionAdapterR5; -import ca.uhn.fhir.jpa.graphql.JpaStorageServices; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR5; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR5; @@ -21,11 +21,9 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR5; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.r5.hapi.ctx.IValidationSupport; -import org.hl7.fhir.r5.hapi.rest.server.GraphQLProvider; import org.hl7.fhir.r5.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r5.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.utils.GraphQLEngine; import org.hl7.fhir.r5.utils.IResourceValidator; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; @@ -85,17 +83,11 @@ public class BaseR5Config extends BaseConfig { return new TransactionProcessor<>(); } -// @Bean(name = GRAPHQL_PROVIDER_NAME) -// @Lazy -// public GraphQLProvider graphQLProvider() { -// return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices()); -// } -// -// @Bean -// @Lazy -// public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() { -// return new JpaStorageServices(); -// } + @Bean(name = GRAPHQL_PROVIDER_NAME) + @Lazy + public GraphQLProvider graphQLProvider() { + return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices()); + } @Bean(name = "myInstanceValidatorR5") @Lazy diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java index 8f8b4783fc6..01636abdd44 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -33,22 +32,21 @@ import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.utils.GraphQLEngine; import org.hl7.fhir.utilities.graphql.Argument; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.graphql.Value; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; -public class JpaStorageServices extends BaseHapiFhirDao implements GraphQLEngine.IGraphQLStorageServices { +public class JpaStorageServices extends BaseHapiFhirDao implements IGraphQLStorageServices { + private static final int MAX_SEARCH_SIZE = 500; private IFhirResourceDao getDao(String theResourceType) { RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType); @@ -57,13 +55,13 @@ public class JpaStorageServices extends BaseHapiFhirDao implement @Transactional(propagation = Propagation.NEVER) @Override - public void listResources(Object theAppInfo, String theType, List theSearchParams, List theMatches) throws FHIRException { + public void listResources(Object theAppInfo, String theType, List theSearchParams, List theMatches) throws FHIRException { RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theType); IFhirResourceDao dao = getDao(typeDef.getImplementingClass()); SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(500); + params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE); for (Argument nextArgument : theSearchParams) { @@ -114,40 +112,37 @@ public class JpaStorageServices extends BaseHapiFhirDao implement size = response.preferredPageSize(); } - for (IBaseResource next : response.getResources(0, size)) { - theMatches.add((Resource) next); - } + theMatches.addAll(response.getResources(0, size)); } @Transactional(propagation = Propagation.REQUIRED) @Override - public Resource lookup(Object theAppInfo, String theType, String theId) throws FHIRException { + public IBaseResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException { IIdType refId = getContext().getVersion().newIdType(); refId.setValue(theType + "/" + theId); return lookup(theAppInfo, refId); } - private Resource lookup(Object theAppInfo, IIdType theRefId) { + private IBaseResource lookup(Object theAppInfo, IIdType theRefId) { IFhirResourceDao dao = getDao(theRefId.getResourceType()); RequestDetails requestDetails = (RequestDetails) theAppInfo; - return (Resource) dao.read(theRefId, requestDetails, false); + return dao.read(theRefId, requestDetails, false); } @Transactional(propagation = Propagation.REQUIRED) - @Override - public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException { - IdType refId = new IdType(theReference.getReference()); - Resource outcome = lookup(theAppInfo, refId); + @Override + public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException { + IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement()); if (outcome == null) { - return null; - } + return null; + } return new ReferenceResolution(theContext, outcome); } @Transactional(propagation = Propagation.NEVER) @Override - public Bundle search(Object theAppInfo, String theType, List theSearchParams) throws FHIRException { + public IBaseBundle search(Object theAppInfo, String theType, List theSearchParams) throws FHIRException { throw new NotImplementedOperationException("Not yet able to handle this GraphQL request"); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java new file mode 100644 index 00000000000..23ac87e0b70 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java @@ -0,0 +1,165 @@ +package ca.uhn.fhir.jpa.provider; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * 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.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.IContextValidationSupport; +import ca.uhn.fhir.rest.annotation.GraphQL; +import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Initialize; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +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 org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +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.Parser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.Supplier; + +public class GraphQLProvider { + private final Supplier engineFactory; + private final IGraphQLStorageServices myStorageServices; + private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); + + /** + * Constructor which uses a default context and validation support object + * + * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) + */ + public GraphQLProvider(IGraphQLStorageServices theStorageServices) { + this(FhirContext.forR4(), null, theStorageServices); + } + + /** + * Constructor which uses the given worker context + * + * @param theFhirContext The HAPI FHIR Context object + * @param theValidationSupport The HAPI Validation Support object, or null + * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) + */ + public GraphQLProvider(@Nonnull FhirContext theFhirContext, @Nullable IContextValidationSupport theValidationSupport, @Nonnull IGraphQLStorageServices theStorageServices) { + Validate.notNull(theFhirContext, "theFhirContext must not be null"); + Validate.notNull(theStorageServices, "theStorageServices must not be null"); + + switch (theFhirContext.getVersion().getVersion()) { + case DSTU3: { + IValidationSupport validationSupport = (IValidationSupport) theValidationSupport; + validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport()); + 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); + break; + } + case R4: { + org.hl7.fhir.r4.hapi.ctx.IValidationSupport validationSupport = (org.hl7.fhir.r4.hapi.ctx.IValidationSupport) theValidationSupport; + validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport()); + 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); + break; + } + case R5: { + org.hl7.fhir.r5.hapi.ctx.IValidationSupport validationSupport = (org.hl7.fhir.r5.hapi.ctx.IValidationSupport) theValidationSupport; + validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.r5.hapi.ctx.DefaultProfileValidationSupport()); + 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); + break; + } + case DSTU2: + case DSTU2_HL7ORG: + case DSTU2_1: + default: { + throw new UnsupportedOperationException("GraphQL not supported for version: " + theFhirContext.getVersion().getVersion()); + } + } + + myStorageServices = theStorageServices; + } + + @GraphQL + public String processGraphQlRequet(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + + IGraphQLEngine engine = engineFactory.get(); + engine.setAppInfo(theRequestDetails); + engine.setServices(myStorageServices); + try { + engine.setGraphQL(Parser.parse(theQuery)); + } catch (Exception theE) { + throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString()); + } + + try { + + if (theId != null) { + IBaseResource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart()); + engine.setFocus(focus); + } + engine.execute(); + + StringBuilder outputBuilder = new StringBuilder(); + ObjectValue output = engine.getOutput(); + output.write(outputBuilder, 0, "\n"); + + return outputBuilder.toString(); + + } catch (Exception e) { + StringBuilder b = new StringBuilder(); + b.append("Unable to execute GraphQL Expression: "); + int statusCode = 500; + if (e instanceof BaseServerResponseException) { + b.append("HTTP "); + statusCode = ((BaseServerResponseException) e).getStatusCode(); + b.append(statusCode); + b.append(" "); + } else { + // This means it's a bug, so let's log + ourLog.error("Failure during GraphQL processing", e); + } + b.append(e.getMessage()); + throw new UnclassifiedServerFailureException(statusCode, b.toString()); + } + } + + @Initialize + public void initialize(RestfulServer theServer) { + ourLog.trace("Initializing GraphQL provider"); + if (!theServer.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context"); + } + } + + +} + diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4ProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java similarity index 90% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4ProviderTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java index 069a4b1564e..34f09a62601 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4ProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/GraphQLR4ProviderTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.rest.server; +package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.OptionalParam; @@ -6,6 +6,10 @@ 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.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.IOUtils; @@ -18,11 +22,14 @@ 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; -import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.utils.GraphQLEngine; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.utilities.graphql.Argument; +import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -34,9 +41,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.*; - -import ca.uhn.fhir.test.utilities.JettyUtil; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class GraphQLR4ProviderTest { @@ -216,9 +222,9 @@ public class GraphQLR4ProviderTest { } - private static class MyStorageServices implements GraphQLEngine.IGraphQLStorageServices { + private static class MyStorageServices implements IGraphQLStorageServices { @Override - public void listResources(Object theAppInfo, String theType, List theSearchParams, List theMatches) throws FHIRException { + public void listResources(Object theAppInfo, String theType, List theSearchParams, List theMatches) throws FHIRException { ourLog.info("listResources of {} - {}", theType, theSearchParams); if (theSearchParams.size() == 1) { @@ -264,8 +270,8 @@ public class GraphQLR4ProviderTest { } @Override - public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException { - ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReference()); + public IGraphQLStorageServices.ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException { + ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReferenceElement().getValue()); return null; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java index 0f5ee99daf4..a0a0cdfa278 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.provider.dstu3; import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; @@ -97,6 +98,8 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { SubscriptionTriggeringProvider subscriptionTriggeringProvider = myAppCtx.getBean(SubscriptionTriggeringProvider.class); ourRestServer.registerProvider(subscriptionTriggeringProvider); + ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); + JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(ourRestServer, mySystemDao, myDaoConfig); confProvider.setImplementationDescription("THIS IS THE DESC"); ourRestServer.setServerConformanceProvider(confProvider); 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 new file mode 100644 index 00000000000..f5cfc3c8b38 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/GraphQLProviderDstu3Test.java @@ -0,0 +1,92 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +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.dstu3.model.Patient; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; + +public class GraphQLProviderDstu3Test extends BaseResourceProviderDstu3Test { + private Logger ourLog = LoggerFactory.getLogger(GraphQLProviderDstu3Test.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.stripReturns("{\n" + + " \"name\":[{\n" + + " \"family\":\"FAM\",\n" + + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + + " },{\n" + + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + + " }]\n" + + "}"), TestUtil.stripReturns(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.stripReturns("{\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" + + "}"), TestUtil.stripReturns(resp)); + } + + } + + private void initTestPatients() { + Patient p = new Patient(); + p.addName() + .setFamily("FAM") + .addGiven("GIVEN1") + .addGiven("GIVEN2"); + p.addName() + .addGiven("GivenOnly1") + .addGiven("GivenOnly2"); + myPatientId0 = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addName() + .addGiven("GivenOnlyB1") + .addGiven("GivenOnlyB2"); + ourClient.create().resource(p).execute(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index a8b3ce9cf70..0a068832f70 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; @@ -71,7 +72,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { protected IGenericClient ourClient; ResourceCountCache ourResourceCountsCache; private TerminologyUploaderProvider myTerminologyUploaderProvider; - private Object ourGraphQLProvider; private boolean ourRestHookSubscriptionInterceptorRequested; @Autowired @@ -105,10 +105,10 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML); myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProvider.class); - ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider"); myDaoRegistry = myAppCtx.getBean(DaoRegistry.class); - ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider, ourGraphQLProvider); + ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider); + ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(ourRestServer, mySystemDao, myDaoConfig); confProvider.setImplementationDescription("THIS IS THE DESC"); 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 index 4bbce8de200..ce32e4675bb 100644 --- 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 @@ -27,20 +27,17 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test { String query = "{name{family,given}}"; HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse response = ourHttpClient.execute(httpGet); - try { + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(resp); - assertEquals(TestUtil.stripReturns(resp), TestUtil.stripReturns("{\n" + + assertEquals(TestUtil.stripReturns("{\n" + " \"name\":[{\n" + " \"family\":\"FAM\",\n" + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + " },{\n" + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + " }]\n" + - "}")); - } finally { - IOUtils.closeQuietly(response); + "}"), TestUtil.stripReturns(resp)); } } @@ -52,8 +49,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test { String query = "{PatientList(given:\"given\"){name{family,given}}}"; HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); - CloseableHttpResponse response = ourHttpClient.execute(httpGet); - try { + try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) { String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(resp); assertEquals(TestUtil.stripReturns("{\n" + @@ -70,10 +66,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test { " }]\n" + " }]\n" + "}"), TestUtil.stripReturns(resp)); - } finally { - IOUtils.closeQuietly(response); } - } private void initTestPatients() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java index 1a3b20e5540..5ddb411c440 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; @@ -108,8 +109,7 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test { myDaoRegistry = myAppCtx.getBean(DaoRegistry.class); ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider); -// ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider"); -// ourRestServer.registerProvider(ourGraphQLProvider); + ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(ourRestServer, mySystemDao, myDaoConfig); confProvider.setImplementationDescription("THIS IS THE DESC"); 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 1d4678891f6..8b543b5baa9 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 @@ -34,7 +34,7 @@ import ca.uhn.fhirtest.config.TestR4Config; import ca.uhn.fhirtest.config.TestR5Config; import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.GraphQLProvider; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -127,6 +127,7 @@ public class TestRestfulServer extends RestfulServer { confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); + providers.add(myAppCtx.getBean(GraphQLProvider.class)); break; } case "R4": { @@ -164,7 +165,7 @@ public class TestRestfulServer extends RestfulServer { confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); -// providers.add(myAppCtx.getBean(GraphQLProvider.class)); + providers.add(myAppCtx.getBean(GraphQLProvider.class)); break; } default: diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 5be2f5dc097..969628e9404 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -19,6 +19,11 @@ hapi-fhir-base ${project.version} + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + ${fhir_core_version} + diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/GraphQLProvider.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/GraphQLProvider.java deleted file mode 100644 index 743b5152920..00000000000 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/GraphQLProvider.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.hl7.fhir.r4.hapi.rest.server; - -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Initialize; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.context.IWorkerContext; -import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; -import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; -import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.utils.GraphQLEngine; -import org.hl7.fhir.utilities.graphql.ObjectValue; -import org.hl7.fhir.utilities.graphql.Parser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class GraphQLProvider { - private final IWorkerContext myWorkerContext; - private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); - private GraphQLEngine.IGraphQLStorageServices myStorageServices; - - /** - * Constructor which uses a default context and validation support object - * - * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) - */ - public GraphQLProvider(GraphQLEngine.IGraphQLStorageServices theStorageServices) { - this(FhirContext.forR4(), new DefaultProfileValidationSupport(), theStorageServices); - } - - /** - * Constructor which uses the given worker context - * - * @param theFhirContext The HAPI FHIR Context object - * @param theValidationSupport The HAPI Validation Support object - * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) - */ - public GraphQLProvider(FhirContext theFhirContext, IValidationSupport theValidationSupport, GraphQLEngine.IGraphQLStorageServices theStorageServices) { - myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport); - myStorageServices = theStorageServices; - } - - @GraphQL - public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { - - GraphQLEngine engine = new GraphQLEngine(myWorkerContext); - engine.setAppInfo(theRequestDetails); - engine.setServices(myStorageServices); - try { - engine.setGraphQL(Parser.parse(theQuery)); - } catch (Exception theE) { - throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString()); - } - - try { - - if (theId != null) { - Resource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart()); - engine.setFocus(focus); - } - engine.execute(); - - StringBuilder outputBuilder = new StringBuilder(); - ObjectValue output = engine.getOutput(); - output.write(outputBuilder, 0, "\n"); - - return outputBuilder.toString(); - - } catch (Exception e) { - StringBuilder b = new StringBuilder(); - b.append("Unable to execute GraphQL Expression: "); - int statusCode = 500; - if (e instanceof BaseServerResponseException) { - b.append("HTTP "); - statusCode = ((BaseServerResponseException) e).getStatusCode(); - b.append(statusCode); - b.append(" "); - } else { - // This means it's a bug, so let's log - ourLog.error("Failure during GraphQL processing", e); - } - b.append(e.getMessage()); - throw new UnclassifiedServerFailureException(statusCode, b.toString()); - } - } - - @Initialize - public void initialize(RestfulServer theServer) { - ourLog.trace("Initializing GraphQL provider"); - if (theServer.getFhirContext().getVersion().getVersion() != FhirVersionEnum.R4) { - throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context"); - } - } - - -} - 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 20a8947815f..aac66d97395 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 @@ -16,7 +16,6 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,8 +32,8 @@ public class GraphQLEngineTest { return obs; } - private GraphQLEngine.IGraphQLStorageServices createStorageServices() throws FHIRException { - GraphQLEngine.IGraphQLStorageServices retVal = mock(GraphQLEngine.IGraphQLStorageServices.class); + 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) { @@ -46,7 +45,7 @@ public class GraphQLEngineTest { if (reference.getReference().equalsIgnoreCase("Patient/123")) { Patient p = new Patient(); p.getBirthDateElement().setValueAsString("2011-02-22"); - return new GraphQLEngine.IGraphQLStorageServices.ReferenceResolution(context, p); + return new IGraphQLStorageServices.ReferenceResolution(context, p); } ourLog.info("Not found!"); diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/GraphQLProvider.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/GraphQLProvider.java deleted file mode 100644 index f2d141dbb68..00000000000 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/GraphQLProvider.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.hl7.fhir.r5.hapi.rest.server; - -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Initialize; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r5.context.IWorkerContext; -import org.hl7.fhir.r5.hapi.ctx.DefaultProfileValidationSupport; -import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; -import org.hl7.fhir.r5.hapi.ctx.IValidationSupport; -import org.hl7.fhir.r5.model.Resource; -import org.hl7.fhir.r5.utils.GraphQLEngine; -import org.hl7.fhir.utilities.graphql.ObjectValue; -import org.hl7.fhir.utilities.graphql.Parser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class GraphQLProvider { - private final IWorkerContext myWorkerContext; - private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); - private GraphQLEngine.IGraphQLStorageServices myStorageServices; - - /** - * Constructor which uses a default context and validation support object - * - * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) - */ - public GraphQLProvider(GraphQLEngine.IGraphQLStorageServices theStorageServices) { - this(FhirContext.forR5(), new DefaultProfileValidationSupport(), theStorageServices); - } - - /** - * Constructor which uses the given worker context - * - * @param theFhirContext The HAPI FHIR Context object - * @param theValidationSupport The HAPI Validation Support object - * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) - */ - public GraphQLProvider(FhirContext theFhirContext, IValidationSupport theValidationSupport, GraphQLEngine.IGraphQLStorageServices theStorageServices) { - myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport); - myStorageServices = theStorageServices; - } - - @GraphQL - public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { - - GraphQLEngine engine = new GraphQLEngine(myWorkerContext); - engine.setAppInfo(theRequestDetails); - engine.setServices(myStorageServices); - try { - engine.setGraphQL(Parser.parse(theQuery)); - } catch (Exception theE) { - throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString()); - } - - try { - - if (theId != null) { - Resource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart()); - engine.setFocus(focus); - } - engine.execute(); - - StringBuilder outputBuilder = new StringBuilder(); - ObjectValue output = engine.getOutput(); - output.write(outputBuilder, 0, "\n"); - - return outputBuilder.toString(); - - } catch (Exception e) { - StringBuilder b = new StringBuilder(); - b.append("Unable to execute GraphQL Expression: "); - int statusCode = 500; - if (e instanceof BaseServerResponseException) { - b.append("HTTP "); - statusCode = ((BaseServerResponseException) e).getStatusCode(); - b.append(statusCode); - b.append(" "); - } else { - // This means it's a bug, so let's log - ourLog.error("Failure during GraphQL processing", e); - } - b.append(e.getMessage()); - throw new UnclassifiedServerFailureException(statusCode, b.toString()); - } - } - - @Initialize - public void initialize(RestfulServer theServer) { - ourLog.trace("Initializing GraphQL provider"); - if (theServer.getFhirContext().getVersion().getVersion() != FhirVersionEnum.R5) { - throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context"); - } - } - - -} - diff --git a/src/changes/changes.xml b/src/changes/changes.xml index bc523790d4a..b9f5afed079 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -88,16 +88,28 @@ Support for the new R5 draft resources has been added. This support includes the client, + New Feature: server, and JPA server. Note that these definitions will change as the R5 standard is modified until it is released, so use with caution! + ]]> + New Feature: A new interceptor called - ConsentInterceptor]]> has been added. This interceptor allows + ConsentInterceptor has been added. This interceptor allows JPA based servers to make appropriate consent decisions related to resources that and operations that are being returned. See - Server Security]]> + Server Security for more information. + ]]> + + + New Feature: + The JPA server now supports GraphQL for DSTU3 / R4 / R5 servers. + ]]> Several enhancements have been made to the AuthorizationInterceptor]]>: @@ -105,7 +117,8 @@
  • The interceptor now registers against the STORAGE_PRESHOW_RESOURCES interceptor hook, which allows it to successfully authorize JPA operations that don't actually return resource content, - such as GraphQL responses.
  • + such as GraphQL responses, and resources that have been filtered using the _elements + parameter.
  • The rule list is now cached on a per-request basis, which should improve performance
]]>