Support GraphQL for R3/4/5 (#1424)

* Work on grpahql enhanbcements

* Add some more chars to the sanitizer function

* Add changelog
This commit is contained in:
James Agnew 2019-08-12 08:24:32 -04:00 committed by GitHub
parent 2999a292e6
commit 0c9e5ec1ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 401 additions and 298 deletions

View File

@ -163,7 +163,16 @@ public class UrlUtil {
if (theString != null) { if (theString != null) {
for (int i = 0; i < theString.length(); i++) { for (int i = 0; i < theString.length(); i++) {
char nextChar = theString.charAt(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; return true;
} }
} }
@ -348,7 +357,17 @@ public class UrlUtil {
/** /**
* This method specifically HTML-encodes the &quot; and * This method specifically HTML-encodes the &quot; and
* &lt; characters in order to prevent injection attacks * &lt; characters in order to prevent injection attacks.
*
* The following characters are escaped:
* <ul>
* <li>&apos;</li>
* <li>&quot;</li>
* <li>&lt;</li>
* <li>&gt;</li>
* <li>\n (newline)</li>
* </ul>
*
*/ */
public static String sanitizeUrlPart(CharSequence theString) { public static String sanitizeUrlPart(CharSequence theString) {
if (theString == null) { if (theString == null) {
@ -364,6 +383,10 @@ public class UrlUtil {
char nextChar = theString.charAt(j); char nextChar = theString.charAt(j);
switch (nextChar) { switch (nextChar) {
/*
* NB: If you add a constant here, you also need to add it
* to isNeedsSanitization()!!
*/
case '\'': case '\'':
buffer.append("&apos;"); buffer.append("&apos;");
break; break;
@ -373,8 +396,19 @@ public class UrlUtil {
case '<': case '<':
buffer.append("&lt;"); buffer.append("&lt;");
break; break;
case '>':
buffer.append("&gt;");
break;
case '\n':
buffer.append("&#10;");
break;
case '\r':
buffer.append("&#13;");
break;
default: default:
buffer.append(nextChar); if (nextChar >= ' ') {
buffer.append(nextChar);
}
break; break;
} }

View File

@ -59,4 +59,15 @@ public class UrlUtilTest {
} }
@Test
public void testSanitize() {
assertEquals(" &apos; ", UrlUtil.sanitizeUrlPart(" ' "));
assertEquals(" &lt; ", UrlUtil.sanitizeUrlPart(" < "));
assertEquals(" &gt; ", UrlUtil.sanitizeUrlPart(" > "));
assertEquals(" &quot; ", UrlUtil.sanitizeUrlPart(" \" "));
assertEquals(" &#10; ", UrlUtil.sanitizeUrlPart(" \n "));
assertEquals(" &#13; ", UrlUtil.sanitizeUrlPart(" \r "));
assertEquals(" ", UrlUtil.sanitizeUrlPart(" \0 "));
}
} }

View File

@ -24,10 +24,8 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; 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.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; 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.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.dao.DaoRegistry; 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.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; 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.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import org.hibernate.jpa.HibernatePersistenceProvider; 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.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.*;
@ -109,6 +111,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
public abstract FhirContext fhirContext(); public abstract FhirContext fhirContext();
@Bean
@Lazy
public IGraphQLStorageServices graphqlStorageServices() {
return new JpaStorageServices();
}
@Bean @Bean
public ScheduledExecutorFactoryBean scheduledExecutorService() { public ScheduledExecutorFactoryBean scheduledExecutorService() {
ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean(); ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean();

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3; 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.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
@ -73,6 +74,12 @@ public class BaseDstu3Config extends BaseConfig {
return retVal; return retVal;
} }
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider() {
return new GraphQLProvider(fhirContextDstu3(), validationSupportChainDstu3(), graphqlStorageServices());
}
@Bean @Bean
public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() { public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
return new TransactionProcessorVersionAdapterDstu3(); return new TransactionProcessorVersionAdapterDstu3();

View File

@ -21,12 +21,13 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR4;
import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; 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.CachingValidationSupport;
import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.utils.GraphQLEngine; import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.r5.utils.IResourceValidator; import org.hl7.fhir.r5.utils.IResourceValidator;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -91,12 +92,6 @@ public class BaseR4Config extends BaseConfig {
return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), graphqlStorageServices()); return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), graphqlStorageServices());
} }
@Bean
@Lazy
public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() {
return new JpaStorageServices();
}
@Bean(name = "myInstanceValidatorR4") @Bean(name = "myInstanceValidatorR4")
@Lazy @Lazy
public IValidatorModule instanceValidatorR4() { public IValidatorModule instanceValidatorR4() {

View File

@ -8,7 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.dao.r5.TransactionProcessorVersionAdapterR5; 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.extractor.SearchParamExtractorR5;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR5; 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 ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r5.hapi.ctx.IValidationSupport; 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.CachingValidationSupport;
import org.hl7.fhir.r5.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r5.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.utils.GraphQLEngine;
import org.hl7.fhir.r5.utils.IResourceValidator; import org.hl7.fhir.r5.utils.IResourceValidator;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -85,17 +83,11 @@ public class BaseR5Config extends BaseConfig {
return new TransactionProcessor<>(); return new TransactionProcessor<>();
} }
// @Bean(name = GRAPHQL_PROVIDER_NAME) @Bean(name = GRAPHQL_PROVIDER_NAME)
// @Lazy @Lazy
// public GraphQLProvider graphQLProvider() { public GraphQLProvider graphQLProvider() {
// return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices()); return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices());
// } }
//
// @Bean
// @Lazy
// public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() {
// return new JpaStorageServices();
// }
@Bean(name = "myInstanceValidatorR5") @Bean(name = "myInstanceValidatorR5")
@Lazy @Lazy

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; 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.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.IBundleProvider; 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import org.hl7.fhir.exceptions.FHIRException; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; 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.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.Value; import org.hl7.fhir.utilities.graphql.Value;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implements GraphQLEngine.IGraphQLStorageServices { public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implements IGraphQLStorageServices {
private static final int MAX_SEARCH_SIZE = 500;
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) { private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType); RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType);
@ -57,13 +55,13 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<Resource> theMatches) throws FHIRException { public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theType); RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theType);
IFhirResourceDao<? extends IBaseResource> dao = getDao(typeDef.getImplementingClass()); IFhirResourceDao<? extends IBaseResource> dao = getDao(typeDef.getImplementingClass());
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(500); params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
for (Argument nextArgument : theSearchParams) { for (Argument nextArgument : theSearchParams) {
@ -114,40 +112,37 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
size = response.preferredPageSize(); size = response.preferredPageSize();
} }
for (IBaseResource next : response.getResources(0, size)) { theMatches.addAll(response.getResources(0, size));
theMatches.add((Resource) next);
}
} }
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
@Override @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(); IIdType refId = getContext().getVersion().newIdType();
refId.setValue(theType + "/" + theId); refId.setValue(theType + "/" + theId);
return lookup(theAppInfo, refId); return lookup(theAppInfo, refId);
} }
private Resource lookup(Object theAppInfo, IIdType theRefId) { private IBaseResource lookup(Object theAppInfo, IIdType theRefId) {
IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType()); IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType());
RequestDetails requestDetails = (RequestDetails) theAppInfo; RequestDetails requestDetails = (RequestDetails) theAppInfo;
return (Resource) dao.read(theRefId, requestDetails, false); return dao.read(theRefId, requestDetails, false);
} }
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
@Override @Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException { public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException {
IdType refId = new IdType(theReference.getReference()); IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement());
Resource outcome = lookup(theAppInfo, refId);
if (outcome == null) { if (outcome == null) {
return null; return null;
} }
return new ReferenceResolution(theContext, outcome); return new ReferenceResolution(theContext, outcome);
} }
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public Bundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException { public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
throw new NotImplementedOperationException("Not yet able to handle this GraphQL request"); throw new NotImplementedOperationException("Not yet able to handle this GraphQL request");
} }
} }

View File

@ -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<IGraphQLEngine> 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");
}
}
}

View File

@ -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.context.FhirContext;
import ca.uhn.fhir.rest.annotation.OptionalParam; 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.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.param.TokenAndListParam; 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.TestUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils; 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.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.exceptions.FHIRException; 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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.utils.GraphQLEngine; 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.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@ -34,9 +41,8 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import ca.uhn.fhir.test.utilities.JettyUtil;
public class GraphQLR4ProviderTest { public class GraphQLR4ProviderTest {
@ -216,9 +222,9 @@ public class GraphQLR4ProviderTest {
} }
private static class MyStorageServices implements GraphQLEngine.IGraphQLStorageServices { private static class MyStorageServices implements IGraphQLStorageServices {
@Override @Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<Resource> theMatches) throws FHIRException { public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
ourLog.info("listResources of {} - {}", theType, theSearchParams); ourLog.info("listResources of {} - {}", theType, theSearchParams);
if (theSearchParams.size() == 1) { if (theSearchParams.size() == 1) {
@ -264,8 +270,8 @@ public class GraphQLR4ProviderTest {
} }
@Override @Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException { public IGraphQLStorageServices.ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException {
ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReference()); ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReferenceElement().getValue());
return null; return null;
} }

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; 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.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -97,6 +98,8 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
SubscriptionTriggeringProvider subscriptionTriggeringProvider = myAppCtx.getBean(SubscriptionTriggeringProvider.class); SubscriptionTriggeringProvider subscriptionTriggeringProvider = myAppCtx.getBean(SubscriptionTriggeringProvider.class);
ourRestServer.registerProvider(subscriptionTriggeringProvider); ourRestServer.registerProvider(subscriptionTriggeringProvider);
ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(ourRestServer, mySystemDao, myDaoConfig); JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC"); confProvider.setImplementationDescription("THIS IS THE DESC");
ourRestServer.setServerConformanceProvider(confProvider); ourRestServer.setServerConformanceProvider(confProvider);

View File

@ -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();
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; 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.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -71,7 +72,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected IGenericClient ourClient; protected IGenericClient ourClient;
ResourceCountCache ourResourceCountsCache; ResourceCountCache ourResourceCountsCache;
private TerminologyUploaderProvider myTerminologyUploaderProvider; private TerminologyUploaderProvider myTerminologyUploaderProvider;
private Object ourGraphQLProvider;
private boolean ourRestHookSubscriptionInterceptorRequested; private boolean ourRestHookSubscriptionInterceptorRequested;
@Autowired @Autowired
@ -105,10 +105,10 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML); ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProvider.class); myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProvider.class);
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
myDaoRegistry = myAppCtx.getBean(DaoRegistry.class); 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); JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC"); confProvider.setImplementationDescription("THIS IS THE DESC");

View File

@ -27,20 +27,17 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
String query = "{name{family,given}}"; String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet); try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
assertEquals(TestUtil.stripReturns(resp), TestUtil.stripReturns("{\n" + assertEquals(TestUtil.stripReturns("{\n" +
" \"name\":[{\n" + " \"name\":[{\n" +
" \"family\":\"FAM\",\n" + " \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" + " \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" + " },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" + " \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" + " }]\n" +
"}")); "}"), TestUtil.stripReturns(resp));
} finally {
IOUtils.closeQuietly(response);
} }
} }
@ -52,8 +49,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
String query = "{PatientList(given:\"given\"){name{family,given}}}"; String query = "{PatientList(given:\"given\"){name{family,given}}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet); try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp); ourLog.info(resp);
assertEquals(TestUtil.stripReturns("{\n" + assertEquals(TestUtil.stripReturns("{\n" +
@ -70,10 +66,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
" }]\n" + " }]\n" +
" }]\n" + " }]\n" +
"}"), TestUtil.stripReturns(resp)); "}"), TestUtil.stripReturns(resp));
} finally {
IOUtils.closeQuietly(response);
} }
} }
private void initTestPatients() { private void initTestPatients() {

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test; 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.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -108,8 +109,7 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test {
myDaoRegistry = myAppCtx.getBean(DaoRegistry.class); myDaoRegistry = myAppCtx.getBean(DaoRegistry.class);
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider); ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider);
// ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider"); ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
// ourRestServer.registerProvider(ourGraphQLProvider);
JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(ourRestServer, mySystemDao, myDaoConfig); JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC"); confProvider.setImplementationDescription("THIS IS THE DESC");

View File

@ -34,7 +34,7 @@ import ca.uhn.fhirtest.config.TestR4Config;
import ca.uhn.fhirtest.config.TestR5Config; import ca.uhn.fhirtest.config.TestR5Config;
import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor;
import org.apache.commons.lang3.StringUtils; 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.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -127,6 +127,7 @@ public class TestRestfulServer extends RestfulServer {
confProvider.setImplementationDescription(implDesc); confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider); setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
providers.add(myAppCtx.getBean(GraphQLProvider.class));
break; break;
} }
case "R4": { case "R4": {
@ -164,7 +165,7 @@ public class TestRestfulServer extends RestfulServer {
confProvider.setImplementationDescription(implDesc); confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider); setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
// providers.add(myAppCtx.getBean(GraphQLProvider.class)); providers.add(myAppCtx.getBean(GraphQLProvider.class));
break; break;
} }
default: default:

View File

@ -19,6 +19,11 @@
<artifactId>hapi-fhir-base</artifactId> <artifactId>hapi-fhir-base</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.utilities</artifactId>
<version>${fhir_core_version}</version>
</dependency>
<!-- Server --> <!-- Server -->
<dependency> <dependency>

View File

@ -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");
}
}
}

View File

@ -16,7 +16,6 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -33,8 +32,8 @@ public class GraphQLEngineTest {
return obs; return obs;
} }
private GraphQLEngine.IGraphQLStorageServices createStorageServices() throws FHIRException { private IGraphQLStorageServices createStorageServices() throws FHIRException {
GraphQLEngine.IGraphQLStorageServices retVal = mock(GraphQLEngine.IGraphQLStorageServices.class); IGraphQLStorageServices retVal = mock(IGraphQLStorageServices.class);
when(retVal.lookup(nullable(Object.class), nullable(Resource.class), nullable(Reference.class))).thenAnswer(new Answer<Object>() { when(retVal.lookup(nullable(Object.class), nullable(Resource.class), nullable(Reference.class))).thenAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
@ -46,7 +45,7 @@ public class GraphQLEngineTest {
if (reference.getReference().equalsIgnoreCase("Patient/123")) { if (reference.getReference().equalsIgnoreCase("Patient/123")) {
Patient p = new Patient(); Patient p = new Patient();
p.getBirthDateElement().setValueAsString("2011-02-22"); p.getBirthDateElement().setValueAsString("2011-02-22");
return new GraphQLEngine.IGraphQLStorageServices.ReferenceResolution(context, p); return new IGraphQLStorageServices.ReferenceResolution(context, p);
} }
ourLog.info("Not found!"); ourLog.info("Not found!");

View File

@ -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");
}
}
}

View File

@ -88,16 +88,28 @@
</action> </action>
<action type="add"> <action type="add">
Support for the new R5 draft resources has been added. This support includes the client, Support for the new R5 draft resources has been added. This support includes the client,
<![CDATA[
<b>New Feature</b>:
server, and JPA server. Note that these definitions will change as the R5 standard is server, and JPA server. Note that these definitions will change as the R5 standard is
modified until it is released, so use with caution! modified until it is released, so use with caution!
]]>
</action> </action>
<action type="add"> <action type="add">
<![CDATA[
<b>New Feature</b>:
A new interceptor called A new interceptor called
<![CDATA[<code>ConsentInterceptor</code>]]> has been added. This interceptor allows <code>ConsentInterceptor</code> has been added. This interceptor allows
JPA based servers to make appropriate consent decisions related to resources that JPA based servers to make appropriate consent decisions related to resources that
and operations that are being returned. See and operations that are being returned. See
<![CDATA[<a href="http://hapifhir.io/doc_rest_server_security.html">Server Security</a>]]> <a href="http://hapifhir.io/doc_rest_server_security.html">Server Security</a>
for more information. for more information.
]]>
</action>
<action type="add">
<![CDATA[
<b>New Feature</b>:
The JPA server now supports GraphQL for DSTU3 / R4 / R5 servers.
]]>
</action> </action>
<action type="add"> <action type="add">
Several enhancements have been made to the <![CDATA[<code>AuthorizationInterceptor</code>]]>: Several enhancements have been made to the <![CDATA[<code>AuthorizationInterceptor</code>]]>:
@ -105,7 +117,8 @@
<ul> <ul>
<li>The interceptor now registers against the <code>STORAGE_PRESHOW_RESOURCES</code> interceptor hook, <li>The interceptor now registers against the <code>STORAGE_PRESHOW_RESOURCES</code> interceptor hook,
which allows it to successfully authorize JPA operations that don't actually return resource content, which allows it to successfully authorize JPA operations that don't actually return resource content,
such as GraphQL responses.</li> such as GraphQL responses, and resources that have been filtered using the <code>_elements</code>
parameter.</li>
<li> <li>
</li>The rule list is now cached on a per-request basis, which should improve performance</ul> </li>The rule list is now cached on a per-request basis, which should improve performance</ul>
]]> ]]>