Work on GraphQL integration

This commit is contained in:
James Agnew 2017-08-14 18:12:58 -04:00
parent 1c88fd154d
commit 5e0a7672b7
19 changed files with 3239 additions and 1767 deletions

View File

@ -22,6 +22,9 @@ package ca.uhn.fhir.jpa.config;
import javax.annotation.Resource;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import org.hl7.fhir.r4.utils.GraphQLEngine;
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.ApplicationContext;
@ -113,4 +116,9 @@ public class BaseConfig implements SchedulingConfigurer {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public IGraphQLStorageServices jpaStorageServices() {
return new JpaStorageServices();
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.config.r4;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel;
@ -78,6 +79,12 @@ public class BaseR4Config extends BaseConfig {
return searchDao;
}
@Bean(name = "myGraphQLProvider")
@Lazy
public GraphQLProvider graphQLProvider() {
return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), jpaStorageServices());
}
@Bean(autowire = Autowire.BY_TYPE)
public SearchParamExtractorR4 searchParamExtractor() {
return new SearchParamExtractorR4();

View File

@ -0,0 +1,121 @@
package ca.uhn.fhir.jpa.graphql;
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.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
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.*;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.ReferenceResolution;
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<IBaseResource> implements IGraphQLStorageServices<IAnyResource, IBaseReference, IBaseBundle> {
@Transactional(propagation = Propagation.REQUIRED)
@Override
public ReferenceResolution<IAnyResource> lookup(Object theAppInfo, IAnyResource theContext, IBaseReference theReference) throws FHIRException {
IIdType refId = theReference.getReferenceElement();
String resourceType = refId.getResourceType();
IFhirResourceDao<? extends IBaseResource> dao = getDao(resourceType);
BaseHasResource id = dao.readEntity(refId);
IBaseResource resource = toResource(id, false);
return new ReferenceResolution<>(theContext, (IAnyResource) resource);
}
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType);
return getDao(typeDef.getImplementingClass());
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public IAnyResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
IIdType refId = getContext().getVersion().newIdType();
refId.setValue(theType + "/" + theId);
IFhirResourceDao<? extends IBaseResource> dao = getDao(theType);
BaseHasResource id = dao.readEntity(refId);
return (IAnyResource) toResource(id, false);
}
@Transactional(propagation = Propagation.NEVER)
@Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IAnyResource> theMatches) throws FHIRException {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theType);
IFhirResourceDao<? extends IBaseResource> dao = getDao(typeDef.getImplementingClass());
SearchParameterMap params = new SearchParameterMap();
for (Argument nextArgument : theSearchParams) {
RuntimeSearchParam searchParam = getSearchParamByName(typeDef, nextArgument.getName());
for (Value nextValue : nextArgument.getValues()) {
String value = nextValue.getValue();
IQueryParameterType param = null;
switch (searchParam.getParamType()){
case NUMBER:
param = new NumberParam(value);
break;
case DATE:
param = new DateParam(value);
break;
case STRING:
param = new StringParam(value);
break;
case TOKEN:
param = new TokenParam(null, value);
break;
case REFERENCE:
param = new ReferenceParam(value);
break;
case COMPOSITE:
throw new InvalidRequestException("Composite parameters are not yet supported in GraphQL");
case QUANTITY:
param = new QuantityParam(value);
break;
case URI:
break;
}
params.add(nextArgument.getName(), param);
}
}
IBundleProvider response = dao.search(params);
int size = response.size();
if (response.preferredPageSize() != null && response.preferredPageSize() < size){
size = response.preferredPageSize();
}
for (IBaseResource next : response.getResources(0, size)){
theMatches.add((Resource) next);
}
}
@Transactional(propagation = Propagation.NEVER)
@Override
public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
throw new NotImplementedOperationException("Not yet able to handle this GraphQL request");
}
}

View File

@ -23,6 +23,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -88,7 +89,8 @@ public abstract class BaseSubscriptionInterceptor extends ServerOperationInterce
myIdToSubscription.put(nextId, resource);
}
for (String next : myIdToSubscription.keySet()) {
for (Enumeration<String> keyEnum = myIdToSubscription.keys(); keyEnum.hasMoreElements(); ) {
String next = keyEnum.nextElement();
if (!allIds.contains(next)) {
myIdToSubscription.remove(next);
}

View File

@ -11,10 +11,13 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Patient;
import org.junit.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.*;
@ -56,6 +59,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected static RestHookSubscriptionR4Interceptor ourRestHookSubscriptionInterceptor;
protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
private Object ourGraphQLProvider;
public BaseResourceProviderR4Test() {
super();
@ -85,8 +89,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderR4.class);
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
ourRestServer.setPlainProviders(mySystemProvider, myTerminologyUploaderProvider);
ourRestServer.setPlainProviders(mySystemProvider, myTerminologyUploaderProvider, ourGraphQLProvider);
JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC");
@ -106,9 +111,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourWebApplicationContext = new GenericWebApplicationContext();
ourWebApplicationContext.setParent(myAppCtx);
ourWebApplicationContext.refresh();
// ContextLoaderListener loaderListener = new ContextLoaderListener(webApplicationContext);
// loaderListener.initWebApplicationContext(mock(ServletContext.class));
//
proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext);
DispatcherServlet dispatcherServlet = new DispatcherServlet();

View File

@ -0,0 +1,98 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.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 GraphQLProviderR4Test extends BaseResourceProviderR4Test {
private Logger ourLog = LoggerFactory.getLogger(GraphQLProviderR4Test.class);
private IIdType myPatientId0;
@Test
public void testInstanceSimpleRead() throws IOException {
initTestPatients();
String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet);
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(resp, "{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}");
} finally {
IOUtils.closeQuietly(response);
}
}
@Test
public void testSystemSimpleSearch() throws IOException {
initTestPatients();
String query = "{PatientList(given:\"given\"){name{family,given}}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet);
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(resp, "{\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" +
"}");
} finally {
IOUtils.closeQuietly(response);
}
}
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

@ -0,0 +1,94 @@
package org.hl7.fhir.dstu3.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.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.dstu3.context.IWorkerContext;
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.HapiWorkerContext;
import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.utils.GraphQLEngine;
import org.hl7.fhir.instance.model.api.IIdType;
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;
public class GraphQLProviderDstu3 {
private final IWorkerContext myWorkerContext;
private Logger ourLog = LoggerFactory.getLogger(GraphQLProviderDstu3.class);
private IGraphQLStorageServices<Resource, Reference, Bundle> 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 GraphQLProviderDstu3(IGraphQLStorageServices<Resource, Reference, Bundle> theStorageServices) {
this(FhirContext.forDstu3(), 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 GraphQLProviderDstu3(FhirContext theFhirContext, IValidationSupport theValidationSupport, IGraphQLStorageServices<Resource, Reference, Bundle> theStorageServices) {
myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport);
myStorageServices = theStorageServices;
}
@Initialize
public void initialize(RestfulServer theServer) {
ourLog.trace("Initializing GraphQL provider");
if (theServer.getFhirContext().getVersion().getVersion() != FhirVersionEnum.DSTU3) {
throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context");
}
}
@GraphQL
public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) {
GraphQLEngine engine = new GraphQLEngine(myWorkerContext);
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 theE) {
throw new InvalidRequestException("Unable to execute GraphQL Expression: " + theE.toString());
}
}
}

View File

@ -660,6 +660,10 @@ private Map<String, Object> userData;
return false;
}
public Property getNamedProperty(String _name) throws FHIRException {
return getChildByName(_name);
}
public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
if (checkValid)
throw new FHIRException("Attempt to read invalid property '"+name+"' on type "+fhirType());

View File

@ -135,6 +135,9 @@ public class Property {
this.structure = structure;
}
public boolean isList() {
return maxCardinality > 1;
}
}

View File

@ -0,0 +1,846 @@
package org.hl7.fhir.dstu3.utils;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.dstu3.context.IWorkerContext;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.graphql.*;
import org.hl7.fhir.utilities.graphql.Operation.OperationType;
import org.hl7.fhir.utilities.graphql.Package;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GraphQLEngine {
public class SearchEdge extends Base {
private BundleEntryComponent be;
private String type;
public SearchEdge(String type, BundleEntryComponent be) {
this.type = type;
this.be = be;
}
@Override
public String fhirType() {
return type;
}
@Override
protected void listChildren(List<Property> result) {
throw new Error("Not Implemented");
}
@Override
public String getIdBase() {
throw new Error("Not Implemented");
}
@Override
public void setIdBase(String value) {
throw new Error("Not Implemented");
}
// @Override
// public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
// switch (_hash) {
// case 3357091: /*mode*/ return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasMode() ? be.getSearch().getModeElement() : null);
// case 109264530: /*score*/ return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasScore() ? be.getSearch().getScoreElement() : null);
// case -341064690: /*resource*/ return new Property(_name, "resource", "n/a", 0, 1, be.hasResource() ? be.getResource() : null);
// default: return super.getNamedProperty(_hash, _name, _checkValid);
// }
// }
}
public class SearchWrapper extends Base {
private Bundle bnd;
private String type;
private Map<String, String> map;
public SearchWrapper(String type, Bundle bnd) throws FHIRException {
this.type = type;
this.bnd = bnd;
for (BundleLinkComponent bl : bnd.getLink())
if (bl.getRelation().equals("self"))
map = parseURL(bl.getUrl());
}
@Override
public String fhirType() {
return type;
}
@Override
protected void listChildren(List<Property> result) {
throw new Error("Not Implemented");
}
@Override
public String getIdBase() {
throw new Error("Not Implemented");
}
@Override
public void setIdBase(String value) {
throw new Error("Not Implemented");
}
// @Override
// public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
// switch (_hash) {
// case 97440432: /*first*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
// case -1273775369: /*previous*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
// case 3377907: /*next*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
// case 3314326: /*last*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
// case 94851343: /*count*/ return new Property(_name, "integer", "n/a", 0, 1, bnd.getTotalElement());
// case -1019779949:/*offset*/ return new Property(_name, "integer", "n/a", 0, 1, extractParam("search-offset"));
// case 860381968: /*pagesize*/ return new Property(_name, "integer", "n/a", 0, 1, extractParam("_count"));
// case 96356950: /*edges*/ return new Property(_name, "edge", "n/a", 0, Integer.MAX_VALUE, getEdges());
// default: return super.getNamedProperty(_hash, _name, _checkValid);
// }
// }
private List<Base> getEdges() {
List<Base> list = new ArrayList<>();
for (BundleEntryComponent be : bnd.getEntry())
list.add(new SearchEdge(type.substring(0, type.length()-10)+"Edge", be));
return list;
}
private Base extractParam(String name) throws FHIRException {
return map != null ? new IntegerType(map.get(name)) : null;
}
private Map<String, String> parseURL(String url) throws FHIRException {
try {
Map<String, String> map = new HashMap<String, String>();
String[] pairs = url.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
String key;
key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
map.put(key, value);
}
return map;
} catch (UnsupportedEncodingException e) {
throw new FHIRException(e);
}
}
private Base extractLink(String _name) throws FHIRException {
for (BundleLinkComponent bl : bnd.getLink()) {
if (bl.getRelation().equals(_name)) {
Map<String, String> map = parseURL(bl.getUrl());
return new StringType(map.get("search-id")+':'+map.get("search-offset"));
}
}
return null;
}
}
private IWorkerContext context;
public GraphQLEngine(IWorkerContext context) {
super();
this.context = context;
}
/**
* for the host to pass context into and get back on the reference resolution interface
*/
private Object appInfo;
/**
* the focus resource - if (there instanceof one. if (there isn"t,) there instanceof no focus
*/
private Resource focus;
/**
* The package that describes the graphQL to be executed, operation name, and variables
*/
private Package graphQL;
/**
* where the output from executing the query instanceof going to go
*/
private ObjectValue output;
/**
* Application provided reference resolution services
*/
private IGraphQLStorageServices<Resource, Reference, Bundle> services;
// internal stuff
private Map<String, Argument> workingVariables = new HashMap<String, Argument>();
public void execute() throws EGraphEngine, EGraphQLException, FHIRException {
if (graphQL == null)
throw new EGraphEngine("Unable to process graphql - graphql document missing");
output = new ObjectValue();
Operation op = null;
// todo: initial conditions
if (!Utilities.noString(graphQL.getOperationName())) {
op = graphQL.getDocument().operation(graphQL.getOperationName());
if (op == null)
throw new EGraphEngine("Unable to find operation \""+graphQL.getOperationName()+"\"");
} else if ((graphQL.getDocument().getOperations().size() == 1))
op = graphQL.getDocument().getOperations().get(0);
else
throw new EGraphQLException("No operation name provided, so expected to find a single operation");
if (op.getOperationType() == OperationType.qglotMutation)
throw new EGraphQLException("Mutation operations are not supported (yet)");
checkNoDirectives(op.getDirectives());
processVariables(op);
if (focus == null)
processSearch(output, op.getSelectionSet());
else
processObject(focus, focus, output, op.getSelectionSet());
}
private boolean checkBooleanDirective(Directive dir) throws EGraphQLException {
if (dir.getArguments().size() != 1)
throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\"");
if (!dir.getArguments().get(0).getName().equals("if"))
throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\"");
List<Value> vl = resolveValues(dir.getArguments().get(0), 1);
return vl.get(0).toString().equals("true");
}
private boolean checkDirectives(List<Directive> directives) throws EGraphQLException {
Directive skip = null;
Directive include = null;
for (Directive dir : directives) {
if (dir.getName().equals("skip")) {
if ((skip == null))
skip = dir;
else
throw new EGraphQLException("Duplicate @skip directives");
} else if (dir.getName().equals("include")) {
if ((include == null))
include = dir;
else
throw new EGraphQLException("Duplicate @include directives");
}
else
throw new EGraphQLException("Directive \""+dir.getName()+"\" instanceof not recognised");
}
if ((skip != null && include != null))
throw new EGraphQLException("Cannot mix @skip and @include directives");
if (skip != null)
return !checkBooleanDirective(skip);
else if (include != null)
return checkBooleanDirective(include);
else
return true;
}
private void checkNoDirectives(List<Directive> directives) {
}
private boolean targetTypeOk(List<Argument> arguments, Resource dest) throws EGraphQLException {
List<String> list = new ArrayList<String>();
for (Argument arg : arguments) {
if ((arg.getName().equals("type"))) {
List<Value> vl = resolveValues(arg);
for (Value v : vl)
list.add(v.toString());
}
}
if (list.size() == 0)
return true;
else
return list.indexOf(dest.fhirType()) > -1;
}
private boolean hasExtensions(Base obj) {
if (obj instanceof BackboneElement)
return ((BackboneElement) obj).getExtension().size() > 0 || ((BackboneElement) obj).getModifierExtension().size() > 0;
else if (obj instanceof DomainResource)
return ((DomainResource)obj).getExtension().size() > 0 || ((DomainResource)obj).getModifierExtension().size() > 0;
else if (obj instanceof Element)
return ((Element)obj).getExtension().size() > 0;
else
return false;
}
private boolean passesExtensionMode(Base obj, boolean extensionMode) {
if (!obj.isPrimitive())
return !extensionMode;
else if (extensionMode)
return !Utilities.noString(obj.getIdBase()) || hasExtensions(obj);
else
return obj.primitiveValue() != "";
}
private List<Base> filter(Resource context, Property prop, List<Argument> arguments, List<Base> values, boolean extensionMode) throws FHIRException, EGraphQLException {
List<Base> result = new ArrayList<Base>();
if (values.size() > 0) {
StringBuilder fp = new StringBuilder();
for (Argument arg : arguments) {
List<Value> vl = resolveValues(arg);
if ((vl.size() != 1))
throw new EGraphQLException("Incorrect number of arguments");
if (values.get(0).isPrimitive())
throw new EGraphQLException("Attempt to use a filter ("+arg.getName()+") on a primtive type ("+prop.getTypeCode()+")");
if ((arg.getName().equals("fhirpath")))
fp.append(" and "+vl.get(0).toString());
else {
Property p = values.get(0).getNamedProperty(arg.getName());
if (p == null)
throw new EGraphQLException("Attempt to use an unknown filter ("+arg.getName()+") on a type ("+prop.getTypeCode()+")");
fp.append(" and "+arg.getName()+" = '"+vl.get(0).toString()+"'");
}
}
if (fp.length() == 0)
for (Base v : values) {
if (passesExtensionMode(v, extensionMode))
result.add(v);
} else {
FHIRPathEngine fpe = new FHIRPathEngine(this.context);
ExpressionNode node = fpe.parse(fp.toString().substring(5));
for (Base v : values)
if (passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node))
result.add(v);
}
}
return result;
}
private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException {
List<Resource> result = new ArrayList<Resource>();
if (bnd.getEntry().size() > 0) {
if ((fhirpath == null))
for (BundleEntryComponent be : bnd.getEntry())
result.add(be.getResource());
else {
FHIRPathEngine fpe = new FHIRPathEngine(context);
ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
for (BundleEntryComponent be : bnd.getEntry())
if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node))
result.add(be.getResource());
}
}
return result;
}
private List<Resource> filterResources(Argument fhirpath, List<Resource> list) throws EGraphQLException, FHIRException {
List<Resource> result = new ArrayList<Resource>();
if (list.size() > 0) {
if ((fhirpath == null))
for (Resource v : list)
result.add(v);
else {
FHIRPathEngine fpe = new FHIRPathEngine(context);
ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
for (Resource v : list)
if (fpe.evaluateToBoolean(null, v, v, node))
result.add(v);
}
}
return result;
}
private boolean hasArgument(List<Argument> arguments, String name, String value) {
for (Argument arg : arguments)
if ((arg.getName().equals(name)) && arg.hasValue(value))
return true;
return false;
}
private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode) throws EGraphQLException, FHIRException {
Argument arg = target.addField(sel.getField().getAlias(), prop.isList());
for (Base value : values) {
if (value.isPrimitive() && !extensionMode) {
if (!sel.getField().getSelectionSet().isEmpty())
throw new EGraphQLException("Encountered a selection set on a scalar field type");
processPrimitive(arg, value);
} else {
if (sel.getField().getSelectionSet().isEmpty())
throw new EGraphQLException("No Fields selected on a complex object");
ObjectValue n = new ObjectValue();
arg.addValue(n);
processObject(context, value, n, sel.getField().getSelectionSet());
}
}
}
private void processVariables(Operation op) throws EGraphQLException {
for (Variable varRef : op.getVariables()) {
Argument varDef = null;
for (Argument v : graphQL.getVariables())
if (v.getName().equals(varRef.getName()))
varDef = v;
if (varDef != null)
workingVariables.put(varRef.getName(), varDef); // todo: check type?
else if (varRef.getDefaultValue() != null)
workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue()));
else
throw new EGraphQLException("No value found for variable ");
}
}
private boolean isPrimitive(String typename) {
return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt");
}
private boolean isResourceName(String name, String suffix) {
if (!name.endsWith(suffix))
return false;
name = name.substring(0, name.length()-suffix.length());
return context.getResourceNames().contains(name);
}
private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection) throws EGraphQLException, FHIRException {
for (Selection sel : selection) {
if (sel.getField() != null) {
if (checkDirectives(sel.getField().getDirectives())) {
Property prop = source.getNamedProperty(sel.getField().getName());
if ((prop == null) && sel.getField().getName().startsWith("_"))
prop = source.getNamedProperty(sel.getField().getName().substring(1));
if (prop == null) {
if ((sel.getField().getName().equals("resourceType") && source instanceof Resource))
target.addField("resourceType", false).addValue(new StringValue(source.fhirType()));
else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference")))
processReference(context, source, sel.getField(), target);
else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource))
processReverseReferenceList((Resource) source, sel.getField(), target);
else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource))
processReverseReferenceSearch((Resource) source, sel.getField(), target);
else
throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
} else {
if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_"))
throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
List<Base> vl = filter(context, prop, sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_"));
if (!vl.isEmpty())
processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"));
}
}
} else if (sel.getInlineFragment() != null) {
if (checkDirectives(sel.getInlineFragment().getDirectives())) {
if (Utilities.noString(sel.getInlineFragment().getTypeCondition()))
throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition()))
processObject(context, source, target, sel.getInlineFragment().getSelectionSet());
}
} else if (checkDirectives(sel.getFragmentSpread().getDirectives())) {
Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName());
if (fragment == null)
throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName());
if (Utilities.noString(fragment.getTypeCondition()))
throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
if (source.fhirType().equals(fragment.getTypeCondition()))
processObject(context, source, target, fragment.getSelectionSet());
}
}
}
private void processPrimitive(Argument arg, Base value) {
String s = value.fhirType();
if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt"))
arg.addValue(new NumberValue(value.primitiveValue()));
else if (s.equals("boolean"))
arg.addValue(new NameValue(value.primitiveValue()));
else
arg.addValue(new StringValue(value.primitiveValue()));
}
private void processReference(Resource context, Base source, Field field, ObjectValue target) throws EGraphQLException, FHIRException {
if (!(source instanceof Reference))
throw new EGraphQLException("Not done yet");
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
Reference ref = (Reference) source;
ReferenceResolution<Resource> res = services.lookup(appInfo, context, ref);
if (res != null) {
if (targetTypeOk(field.getArguments(), res.getTarget())) {
Argument arg = target.addField(field.getAlias(), false);
ObjectValue obj = new ObjectValue();
arg.addValue(obj);
processObject(res.getTargetContext(), res.getTarget(), obj, field.getSelectionSet());
}
}
else if (!hasArgument(field.getArguments(), "optional", "true"))
throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
}
private void processReverseReferenceList(Resource source, Field field, ObjectValue target) throws EGraphQLException, FHIRException {
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
List<Resource> list = new ArrayList<Resource>();
List<Argument> params = new ArrayList<Argument>();
Argument parg = null;
for (Argument a : field.getArguments())
if (!(a.getName().equals("_reference")))
params.add(a);
else if ((parg == null))
parg = a;
else
throw new EGraphQLException("Duplicate parameter _reference");
if (parg == null)
throw new EGraphQLException("Missing parameter _reference");
Argument arg = new Argument();
params.add(arg);
arg.setName(getSingleValue(parg));
arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list);
arg = null;
ObjectValue obj = null;
List<Resource> vl = filterResources(field.argument("fhirpath"), list);
if (!vl.isEmpty()) {
arg = target.addField(field.getAlias(), true);
for (Resource v : vl) {
obj = new ObjectValue();
arg.addValue(obj);
processObject(v, v, obj, field.getSelectionSet());
}
}
}
private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target) throws EGraphQLException, FHIRException {
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
List<Argument> params = new ArrayList<Argument>();
Argument parg = null;
for (Argument a : field.getArguments())
if (!(a.getName().equals("_reference")))
params.add(a);
else if ((parg == null))
parg = a;
else
throw new EGraphQLException("Duplicate parameter _reference");
if (parg == null)
throw new EGraphQLException("Missing parameter _reference");
Argument arg = new Argument();
params.add(arg);
arg.setName(getSingleValue(parg));
arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
Base bndWrapper = new SearchWrapper(field.getName(), bnd);
arg = target.addField(field.getAlias(), false);
ObjectValue obj = new ObjectValue();
arg.addValue(obj);
processObject(null, bndWrapper, obj, field.getSelectionSet());
}
private void processSearch(ObjectValue target, List<Selection> selection) throws EGraphQLException, FHIRException {
for (Selection sel : selection) {
if ((sel.getField() == null))
throw new EGraphQLException("Only field selections are allowed in this context");
checkNoDirectives(sel.getField().getDirectives());
if ((isResourceName(sel.getField().getName(), "")))
processSearchSingle(target, sel.getField());
else if ((isResourceName(sel.getField().getName(), "List")))
processSearchSimple(target, sel.getField());
else if ((isResourceName(sel.getField().getName(), "Connection")))
processSearchFull(target, sel.getField());
}
}
private void processSearchSingle(ObjectValue target, Field field) throws EGraphQLException, FHIRException {
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
String id = "";
for (Argument arg : field.getArguments())
if ((arg.getName().equals("id")))
id = getSingleValue(arg);
else
throw new EGraphQLException("Unknown/invalid parameter "+arg.getName());
if (Utilities.noString(id))
throw new EGraphQLException("No id found");
Resource res = services.lookup(appInfo, field.getName(), id);
if (res == null)
throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found");
Argument arg = target.addField(field.getAlias(), false);
ObjectValue obj = new ObjectValue();
arg.addValue(obj);
processObject(res, res, obj, field.getSelectionSet());
}
private void processSearchSimple(ObjectValue target, Field field) throws EGraphQLException, FHIRException {
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
List<Resource> list = new ArrayList<Resource>();
services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list);
Argument arg = null;
ObjectValue obj = null;
List<Resource> vl = filterResources(field.argument("fhirpath"), list);
if (!vl.isEmpty()) {
arg = target.addField(field.getAlias(), true);
for (Resource v : vl) {
obj = new ObjectValue();
arg.addValue(obj);
processObject(v, v, obj, field.getSelectionSet());
}
}
}
private void processSearchFull(ObjectValue target, Field field) throws EGraphQLException, FHIRException {
if (services == null)
throw new EGraphQLException("Resource Referencing services not provided");
List<Argument> params = new ArrayList<Argument>();
Argument carg = null;
for ( Argument arg : field.getArguments())
if (arg.getName().equals("cursor"))
carg = arg;
else
params.add(arg);
if ((carg != null)) {
params.clear();;
String[] parts = getSingleValue(carg).split(":");
params.add(new Argument("search-id", new StringValue(parts[0])));
params.add(new Argument("search-offset", new StringValue(parts[1])));
}
Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd);
Argument arg = target.addField(field.getAlias(), false);
ObjectValue obj = new ObjectValue();
arg.addValue(obj);
processObject(null, bndWrapper, obj, field.getSelectionSet());
}
private String getSingleValue(Argument arg) throws EGraphQLException {
List<Value> vl = resolveValues(arg, 1);
if (vl.size() == 0)
return "";
return vl.get(0).toString();
}
private List<Value> resolveValues(Argument arg) throws EGraphQLException {
return resolveValues(arg, -1, "");
}
private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException {
return resolveValues(arg, max, "");
}
private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException {
List<Value> result = new ArrayList<Value>();
for (Value v : arg.getValues()) {
if (! (v instanceof VariableValue))
result.add(v);
else {
if (vars.contains(":"+v.toString()+":"))
throw new EGraphQLException("Recursive reference to variable "+v.toString());
Argument a = workingVariables.get(v.toString());
if (a == null)
throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\"");
List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":");
result.addAll(vl);
}
}
if ((max != -1 && result.size() > max))
throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered");
return result;
}
public Object getAppInfo() {
return appInfo;
}
public void setAppInfo(Object appInfo) {
this.appInfo = appInfo;
}
public Resource getFocus() {
return focus;
}
public void setFocus(Resource focus) {
this.focus = focus;
}
public Package getGraphQL() {
return graphQL;
}
public void setGraphQL(Package graphQL) {
this.graphQL = graphQL;
}
public ObjectValue getOutput() {
return output;
}
public IGraphQLStorageServices getServices() {
return services;
}
public void setServices(IGraphQLStorageServices services) {
this.services = services;
}
//
//{ GraphQLSearchWrapper }
//
//constructor GraphQLSearchWrapper.Create(bundle : Bundle);
//var
// s : String;
//{
// inherited Create;
// FBundle = bundle;
// s = bundle_List.Matches["self"];
// FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1));
//}
//
//destructor GraphQLSearchWrapper.Destroy;
//{
// FParseMap.free;
// FBundle.Free;
// inherited;
//}
//
//function GraphQLSearchWrapper.extractLink(name: String): String;
//var
// s : String;
// pm : TParseMap;
//{
// s = FBundle_List.Matches[name];
// if (s == "")
// result = null
// else
// {
// pm = TParseMap.create(s.Substring(s.IndexOf("?")+1));
// try
// result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset"));
// finally
// pm.Free;
// }
// }
//}
//
//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base;
//var
// s : String;
//{
// s = FParseMap.GetVar(name);
// if (s == "")
// result = null
// else if (int)
// result = Integer.Create(s)
// else
// result = String.Create(s);
//}
//
//function GraphQLSearchWrapper.fhirType(): String;
//{
// result = "*Connection";
//}
//
// // http://test.fhir.org/r3/Patient?_format==text/xhtml&search-id==77c97e03-8a6c-415f-a63d-11c80cf73f&&active==true&_sort==_id&search-offset==50&_count==50
//
//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property;
//var
// list : List<GraphQLSearchEdge>;
// be : BundleEntry;
//{
// if (propName == "first")
// result = Property.Create(self, propname, "string", false, String, extractLink("first"))
// else if (propName == "previous")
// result = Property.Create(self, propname, "string", false, String, extractLink("previous"))
// else if (propName == "next")
// result = Property.Create(self, propname, "string", false, String, extractLink("next"))
// else if (propName == "last")
// result = Property.Create(self, propname, "string", false, String, extractLink("last"))
// else if (propName == "count")
// result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement)
// else if (propName == "offset")
// result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true))
// else if (propName == "pagesize")
// result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true))
// else if (propName == "edges")
// {
// list = ArrayList<GraphQLSearchEdge>();
// try
// for be in FBundle.getEntry() do
// list.add(GraphQLSearchEdge.create(be));
// result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list));
// finally
// list.Free;
// }
// }
// else
// result = null;
//}
//
//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle);
//{
// FBundle.Free;
// FBundle = Value;
//}
//
//{ GraphQLSearchEdge }
//
//constructor GraphQLSearchEdge.Create(entry: BundleEntry);
//{
// inherited Create;
// FEntry = entry;
//}
//
//destructor GraphQLSearchEdge.Destroy;
//{
// FEntry.Free;
// inherited;
//}
//
//function GraphQLSearchEdge.fhirType(): String;
//{
// result = "*Edge";
//}
//
//function GraphQLSearchEdge.getPropertyValue(propName: string): Property;
//{
// if (propName == "mode")
// {
// if (FEntry.search != null)
// result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement)
// else
// result = Property.Create(self, propname, "code", false, Enum, Base(null));
// }
// else if (propName == "score")
// {
// if (FEntry.search != null)
// result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement)
// else
// result = Property.Create(self, propname, "decimal", false, Decimal, Base(null));
// }
// else if (propName == "resource")
// result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource())
// else
// result = null;
//}
//
//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry);
//{
// FEntry.Free;
// FEntry = value;
//}
//
}

View File

@ -0,0 +1,280 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.dstu3.hapi.rest.server.GraphQLProviderDstu3;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.ReferenceResolution;
import org.junit.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class GraphQLDstu3ProviderTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLDstu3ProviderTest.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static int ourPort;
private static Server ourServer;
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.registerProvider(new DummyPatientResourceProvider());
MyStorageServices storageServices = new MyStorageServices();
servlet.registerProvider(new GraphQLProviderDstu3(storageServices));
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
@Before
public void before() {
//nothing
}
@Test
@Ignore
public void testGraphInstance() throws Exception {
String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("{\n" +
" \"name\":[{\n" +
" \"family\":\"FAMILY\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}", responseContent);
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json"));
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
@Ignore
public void testGraphSystemInstance() throws Exception {
String query = "{Patient(id:123){id,name{given,family}}}";
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("{\n" +
" \"Patient\":{\n" +
" \"name\":[{\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"],\n" +
" \"family\":\"FAMILY\"\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
" }\n" +
"}", responseContent);
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json"));
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
@Ignore
public void testGraphSystemList() throws Exception {
String query = "{PatientList(name:\"pet\"){name{family,given}}}";
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("{\n" +
" \"PatientList\":[{\n" +
" \"name\":[{\n" +
" \"family\":\"pet\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
" },{\n" +
" \"name\":[{\n" +
" \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" +
" }]\n" +
" }]\n" +
"}", responseContent);
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json"));
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
@Ignore
public void testGraphInstanceWithFhirpath() throws Exception {
String query = "{name(fhirpath:\"family.exists()\"){text,given,family}}";
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escape(query));
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("{\n" +
" \"name\":[{\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"],\n" +
" \"family\":\"FAMILY\"\n" +
" }]\n" +
"}", responseContent);
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json"));
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public List search(
@OptionalParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) {
ArrayList<Patient> retVal = new ArrayList<>();
for (int i = 0; i < 200; i++) {
Patient patient = new Patient();
patient.addName(new HumanName().setFamily("FAMILY"));
patient.getIdElement().setValue("Patient/" + i);
retVal.add((Patient) patient);
}
return retVal;
}
}
private static class MyStorageServices implements IGraphQLStorageServices<Resource, Reference, Bundle> {
@Override
public ReferenceResolution<Resource> lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReference());
return null;
}
@Override
public Resource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
ourLog.info("lookup {}/{}", theType, theId);
if (theType.equals("Patient") && theId.equals("123")) {
Patient p = new Patient();
p.addName()
.setFamily("FAMILY")
.addGiven("GIVEN1")
.addGiven("GIVEN2");
p.addName()
.addGiven("GivenOnly1")
.addGiven("GivenOnly2");
return p;
}
return null;
}
@Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<Resource> theMatches) throws FHIRException {
ourLog.info("listResources of {} - {}", theType, theSearchParams);
if (theSearchParams.size() == 1) {
String name = theSearchParams.get(0).getName();
if ("name".equals(name)) {
Patient p = new Patient();
p.addName()
.setFamily(theSearchParams.get(0).getValues().get(0).toString())
.addGiven("GIVEN1")
.addGiven("GIVEN2");
p.addName()
.addGiven("GivenOnly1")
.addGiven("GivenOnly2");
theMatches.add(p);
p = new Patient();
p.addName()
.addGiven("GivenOnlyB1")
.addGiven("GivenOnlyB2");
theMatches.add(p);
}
}
}
@Override
public Bundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
ourLog.info("search on {} - {}", theType, theSearchParams);
return null;
}
}
}

View File

@ -15,8 +15,11 @@ 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.Bundle;
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.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Parser;
import org.slf4j.Logger;
@ -25,14 +28,14 @@ import org.slf4j.LoggerFactory;
public class GraphQLProvider {
private final IWorkerContext myWorkerContext;
private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class);
private GraphQLEngine.IGraphQLStorageServices myStorageServices;
private IGraphQLStorageServices<Resource, Reference, Bundle> 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) {
public GraphQLProvider(IGraphQLStorageServices<Resource, Reference, Bundle> theStorageServices) {
this(FhirContext.forR4(), new DefaultProfileValidationSupport(), theStorageServices);
}
@ -43,7 +46,7 @@ public class GraphQLProvider {
* @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) {
public GraphQLProvider(FhirContext theFhirContext, IValidationSupport theValidationSupport, IGraphQLStorageServices<Resource, Reference, Bundle> theStorageServices) {
myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport);
myStorageServices = theStorageServices;
}

View File

@ -1,53 +1,22 @@
package org.hl7.fhir.r4.utils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.graphql.*;
import org.hl7.fhir.utilities.graphql.Operation.OperationType;
import org.hl7.fhir.utilities.graphql.Package;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.utilities.graphql.Package;
import org.hl7.fhir.utilities.graphql.Selection;
import org.hl7.fhir.utilities.graphql.StringValue;
import org.hl7.fhir.utilities.graphql.Value;
import org.hl7.fhir.utilities.graphql.Variable;
import org.hl7.fhir.utilities.graphql.VariableValue;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.model.BackboneElement;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent;
import org.hl7.fhir.r4.model.DomainResource;
import org.hl7.fhir.r4.model.Element;
import org.hl7.fhir.r4.model.ExpressionNode;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Property;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.utils.GraphQLEngine.SearchEdge;
import org.hl7.fhir.r4.utils.GraphQLEngine.SearchWrapper;
import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException;
import org.hl7.fhir.r4.utils.GraphQLEngine.IGraphQLStorageServices.ReferenceResolution;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.Directive;
import org.hl7.fhir.utilities.graphql.EGraphEngine;
import org.hl7.fhir.utilities.graphql.EGraphQLException;
import org.hl7.fhir.utilities.graphql.Field;
import org.hl7.fhir.utilities.graphql.Fragment;
import org.hl7.fhir.utilities.graphql.NameValue;
import org.hl7.fhir.utilities.graphql.NumberValue;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Operation;
import org.hl7.fhir.utilities.graphql.Operation.OperationType;
public class GraphQLEngine {
public class SearchEdge extends Base {
@ -179,31 +148,6 @@ public class GraphQLEngine {
}
public interface IGraphQLStorageServices {
public class ReferenceResolution {
private Resource targetContext;
private Resource target;
public ReferenceResolution(Resource targetContext, Resource target) {
super();
this.targetContext = targetContext;
this.target = target;
}
}
// given a reference inside a context, return what it references (including resolving internal references (e.g. start with #)
public ReferenceResolution lookup(Object appInfo, Resource context, Reference reference) throws FHIRException;
// just get the identified resource
public Resource lookup(Object appInfo, String type, String id) throws FHIRException;
// list the matching resources. searchParams are the standard search params.
// this instanceof different to search because the server returns all matching resources, or an error. There instanceof no paging on this search
public void listResources(Object appInfo, String type, List<Argument> searchParams, List<Resource> matches) throws FHIRException;
// just perform a standard search, and return the bundle as you return to the client
public Bundle search(Object appInfo, String type, List<Argument> searchParams) throws FHIRException;
}
private IWorkerContext context;
@ -235,7 +179,7 @@ public class GraphQLEngine {
/**
* Application provided reference resolution services
*/
private IGraphQLStorageServices services;
private IGraphQLStorageServices<Resource, Reference, Bundle> services;
// internal stuff
private Map<String, Argument> workingVariables = new HashMap<String, Argument>();
@ -526,13 +470,13 @@ public class GraphQLEngine {
throw new EGraphQLException("Resource Referencing services not provided");
Reference ref = (Reference) source;
ReferenceResolution res = services.lookup(appInfo, context, ref);
ReferenceResolution<Resource> res = services.lookup(appInfo, context, ref);
if (res != null) {
if (targetTypeOk(field.getArguments(), res.target)) {
if (targetTypeOk(field.getArguments(), res.getTarget())) {
Argument arg = target.addField(field.getAlias(), false);
ObjectValue obj = new ObjectValue();
arg.addValue(obj);
processObject(res.targetContext, res.target, obj, field.getSelectionSet());
processObject(res.getTargetContext(), res.getTarget(), obj, field.getSelectionSet());
}
}
else if (!hasArgument(field.getArguments(), "optional", "true"))

View File

@ -24,6 +24,8 @@ 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.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.ReferenceResolution;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@ -63,7 +65,7 @@ public class GraphQLR4ProviderTest {
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.registerProvider(new DummyPatientResourceProvider());
GraphQLEngine.IGraphQLStorageServices storageServices = new MyStorageServices();
MyStorageServices storageServices = new MyStorageServices();
servlet.registerProvider(new GraphQLProvider(storageServices));
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
@ -216,9 +218,9 @@ public class GraphQLR4ProviderTest {
}
private static class MyStorageServices implements GraphQLEngine.IGraphQLStorageServices {
private static class MyStorageServices implements IGraphQLStorageServices<Resource, Reference, Bundle> {
@Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
public ReferenceResolution<Resource> lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReference());
return null;
}

View File

@ -6,16 +6,11 @@ import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.utilities.graphql.EGraphEngine;
import org.hl7.fhir.utilities.graphql.EGraphQLException;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Parser;
import org.hl7.fhir.utilities.graphql.*;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
@ -104,8 +99,8 @@ public class GraphQLEngineTest {
}
private GraphQLEngine.IGraphQLStorageServices createStorageServices() throws FHIRException {
GraphQLEngine.IGraphQLStorageServices retVal = mock(GraphQLEngine.IGraphQLStorageServices.class);
private IGraphQLStorageServices<Resource, Reference, Bundle> createStorageServices() throws FHIRException {
IGraphQLStorageServices<Resource, Reference, Bundle> retVal = mock(IGraphQLStorageServices.class);
when(retVal.lookup(any(Object.class), any(Resource.class), any(Reference.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
@ -117,7 +112,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 ReferenceResolution<>(context, p);
}
ourLog.info("Not found!");

View File

@ -0,0 +1,33 @@
package org.hl7.fhir.utilities.graphql;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseReference;
import java.util.List;
public interface IGraphQLStorageServices<RT extends IAnyResource, REFT extends IBaseReference, BT extends IBaseBundle> {
/**
* given a reference inside a context, return what it references (including resolving internal references (e.g. start with #)
*/
ReferenceResolution<RT> lookup(Object appInfo, RT context, REFT reference) throws FHIRException;
/**
* just get the identified resource
*/
RT lookup(Object appInfo, String type, String id) throws FHIRException;
/**
* list the matching resources. searchParams are the standard search params.
* this instanceof different to search because the server returns all matching resources, or an error. There instanceof no paging on this search
*/
void listResources(Object appInfo, String type, List<Argument> searchParams, List<RT> matches) throws FHIRException;
/**
* just perform a standard search, and return the bundle as you return to the client
*/
BT search(Object appInfo, String type, List<Argument> searchParams) throws FHIRException;
}

View File

@ -50,6 +50,11 @@ public class ObjectValue extends Value {
write(b, indent, System.lineSeparator());
}
@Override
public String getValue() {
return null;
}
/**
* Write the output using the system default line separator (as defined in {@link System#lineSeparator}
* @param b The StringBuilder to populate

View File

@ -0,0 +1,22 @@
package org.hl7.fhir.utilities.graphql;
public class ReferenceResolution<RT> {
private final RT targetContext;
private final RT target;
public ReferenceResolution(RT targetContext, RT target) {
super();
this.targetContext = targetContext;
this.target = target;
}
public RT getTargetContext() {
return targetContext;
}
public RT getTarget() {
return target;
}
}

View File

@ -2,7 +2,10 @@ package org.hl7.fhir.utilities.graphql;
public abstract class Value {
public abstract void write(StringBuilder b, int indent) throws EGraphEngine, EGraphQLException;
public boolean isValue(String v) {
return false;
}
public abstract String getValue();
}