Work on GraphQL integration
This commit is contained in:
parent
1c88fd154d
commit
5e0a7672b7
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -1,140 +1,143 @@
|
|||
package org.hl7.fhir.dstu3.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A child element or property defined by the FHIR specification
|
||||
* This class is defined as a helper class when iterating the
|
||||
* children of an element in a generic fashion
|
||||
*
|
||||
* At present, iteration is only based on the specification, but
|
||||
* this may be changed to allow profile based expression at a
|
||||
* later date
|
||||
*
|
||||
* note: there's no point in creating one of these classes outside this package
|
||||
*/
|
||||
public class Property {
|
||||
|
||||
/**
|
||||
* The name of the property as found in the FHIR specification
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The type of the property as specified in the FHIR specification (e.g. type|type|Reference(Name|Name)
|
||||
*/
|
||||
private String typeCode;
|
||||
|
||||
/**
|
||||
* The formal definition of the element given in the FHIR specification
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* The minimum allowed cardinality - 0 or 1 when based on the specification
|
||||
*/
|
||||
private int minCardinality;
|
||||
|
||||
/**
|
||||
* The maximum allowed cardinality - 1 or MAX_INT when based on the specification
|
||||
*/
|
||||
private int maxCardinality;
|
||||
|
||||
/**
|
||||
* The actual elements that exist on this instance
|
||||
*/
|
||||
private List<Base> values = new ArrayList<Base>();
|
||||
|
||||
/**
|
||||
* For run time, if/once a property is hooked up to it's definition
|
||||
*/
|
||||
private StructureDefinition structure;
|
||||
|
||||
/**
|
||||
* Internal constructor
|
||||
*/
|
||||
public Property(String name, String typeCode, String definition, int minCardinality, int maxCardinality, Base value) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.typeCode = typeCode;
|
||||
this.definition = definition;
|
||||
this.minCardinality = minCardinality;
|
||||
this.maxCardinality = maxCardinality;
|
||||
this.values.add(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal constructor
|
||||
*/
|
||||
public Property(String name, String typeCode, String definition, int minCardinality, int maxCardinality, List<? extends Base> values) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.typeCode = typeCode;
|
||||
this.definition = definition;
|
||||
this.minCardinality = minCardinality;
|
||||
this.maxCardinality = maxCardinality;
|
||||
if (values != null)
|
||||
this.values.addAll(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of this property in the FHIR Specification
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The stated type in the FHIR specification
|
||||
*/
|
||||
public String getTypeCode() {
|
||||
return typeCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The definition of this element in the FHIR spec
|
||||
*/
|
||||
public String getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minimum cardinality for this element
|
||||
*/
|
||||
public int getMinCardinality() {
|
||||
return minCardinality;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum cardinality for this element
|
||||
*/
|
||||
public int getMaxCardinality() {
|
||||
return maxCardinality;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the actual values - will only be 1 unless maximum cardinality == MAX_INT
|
||||
*/
|
||||
public List<Base> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean hasValues() {
|
||||
for (Base e : getValues())
|
||||
if (e != null)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public StructureDefinition getStructure() {
|
||||
return structure;
|
||||
}
|
||||
|
||||
public void setStructure(StructureDefinition structure) {
|
||||
this.structure = structure;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
package org.hl7.fhir.dstu3.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A child element or property defined by the FHIR specification
|
||||
* This class is defined as a helper class when iterating the
|
||||
* children of an element in a generic fashion
|
||||
*
|
||||
* At present, iteration is only based on the specification, but
|
||||
* this may be changed to allow profile based expression at a
|
||||
* later date
|
||||
*
|
||||
* note: there's no point in creating one of these classes outside this package
|
||||
*/
|
||||
public class Property {
|
||||
|
||||
/**
|
||||
* The name of the property as found in the FHIR specification
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The type of the property as specified in the FHIR specification (e.g. type|type|Reference(Name|Name)
|
||||
*/
|
||||
private String typeCode;
|
||||
|
||||
/**
|
||||
* The formal definition of the element given in the FHIR specification
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* The minimum allowed cardinality - 0 or 1 when based on the specification
|
||||
*/
|
||||
private int minCardinality;
|
||||
|
||||
/**
|
||||
* The maximum allowed cardinality - 1 or MAX_INT when based on the specification
|
||||
*/
|
||||
private int maxCardinality;
|
||||
|
||||
/**
|
||||
* The actual elements that exist on this instance
|
||||
*/
|
||||
private List<Base> values = new ArrayList<Base>();
|
||||
|
||||
/**
|
||||
* For run time, if/once a property is hooked up to it's definition
|
||||
*/
|
||||
private StructureDefinition structure;
|
||||
|
||||
/**
|
||||
* Internal constructor
|
||||
*/
|
||||
public Property(String name, String typeCode, String definition, int minCardinality, int maxCardinality, Base value) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.typeCode = typeCode;
|
||||
this.definition = definition;
|
||||
this.minCardinality = minCardinality;
|
||||
this.maxCardinality = maxCardinality;
|
||||
this.values.add(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal constructor
|
||||
*/
|
||||
public Property(String name, String typeCode, String definition, int minCardinality, int maxCardinality, List<? extends Base> values) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.typeCode = typeCode;
|
||||
this.definition = definition;
|
||||
this.minCardinality = minCardinality;
|
||||
this.maxCardinality = maxCardinality;
|
||||
if (values != null)
|
||||
this.values.addAll(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of this property in the FHIR Specification
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The stated type in the FHIR specification
|
||||
*/
|
||||
public String getTypeCode() {
|
||||
return typeCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The definition of this element in the FHIR spec
|
||||
*/
|
||||
public String getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minimum cardinality for this element
|
||||
*/
|
||||
public int getMinCardinality() {
|
||||
return minCardinality;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum cardinality for this element
|
||||
*/
|
||||
public int getMaxCardinality() {
|
||||
return maxCardinality;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the actual values - will only be 1 unless maximum cardinality == MAX_INT
|
||||
*/
|
||||
public List<Base> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean hasValues() {
|
||||
for (Base e : getValues())
|
||||
if (e != null)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public StructureDefinition getStructure() {
|
||||
return structure;
|
||||
}
|
||||
|
||||
public void setStructure(StructureDefinition structure) {
|
||||
this.structure = structure;
|
||||
}
|
||||
|
||||
public boolean isList() {
|
||||
return maxCardinality > 1;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
//}
|
||||
//
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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!");
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue