Work on graph QL support

This commit is contained in:
James Agnew 2017-08-10 11:36:25 -04:00
parent 1a95ba3b65
commit 5413b276af
18 changed files with 670 additions and 108 deletions

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.rest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A method annotated with this annotation will be treated as a GraphQL implementation
* method
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value= ElementType.METHOD)
public @interface GraphQL {
}

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.rest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation should be placed on the parameter of a
* {@link GraphQL @GraphQL} annotated method. The given
* parameter will be populated with the specific graphQL
* query being requested.
*
* <p>
* This parameter should be of type {@link String}
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface GraphQLQuery {
// ignore
}

View File

@ -79,5 +79,5 @@ public @interface Operation {
* bundle type to set in the bundle. * bundle type to set in the bundle.
*/ */
BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION; BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION;
} }

View File

@ -171,6 +171,7 @@ public class Constants {
public static final String URL_TOKEN_HISTORY = "_history"; public static final String URL_TOKEN_HISTORY = "_history";
public static final String URL_TOKEN_METADATA = "metadata"; public static final String URL_TOKEN_METADATA = "metadata";
public static final String OO_INFOSTATUS_PROCESSING = "processing"; public static final String OO_INFOSTATUS_PROCESSING = "processing";
public static final String PARAM_GRAPHQL_QUERY = "query";
static { static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);

View File

@ -37,6 +37,14 @@ public enum RestOperationTypeEnum {
GET_PAGE("get-page"), GET_PAGE("get-page"),
/**
* <b>
* Use this value with caution, this may
* change as the GraphQL interface matures
* </b>
*/
GRAPHQL_REQUEST("graphql-request"),
/** /**
* E.g. $everything, $validate, etc. * E.g. $everything, $validate, etc.
*/ */

View File

@ -1,11 +1,10 @@
package ca.uhn.fhir.rest.api.server; package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.api.IFhirVersion; import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.IServerConformanceProvider; import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import org.hl7.fhir.instance.model.api.IBaseResource;
/** /**
* This class is the server specific equivalent to {@link IFhirVersion} * This class is the server specific equivalent to {@link IFhirVersion}
@ -15,5 +14,5 @@ public interface IFhirVersionServer {
IServerConformanceProvider<? extends IBaseResource> createServerConformanceProvider(RestfulServer theRestfulServer); IServerConformanceProvider<? extends IBaseResource> createServerConformanceProvider(RestfulServer theRestfulServer);
IResourceProvider createServerProfilesProvider(RestfulServer theRestfulServer); IResourceProvider createServerProfilesProvider(RestfulServer theRestfulServer);
} }

View File

@ -543,7 +543,7 @@ public class RestfulServerUtils {
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStausCode, String theStatusMessage, public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStausCode, String theStatusMessage,
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated) boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
throws IOException { throws IOException {
IRestfulResponse restUtil = theRequestDetails.getResponse(); IRestfulResponse response = theRequestDetails.getResponse();
// Determine response encoding // Determine response encoding
ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theServer.getDefaultResponseEncoding()); ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theServer.getDefaultResponseEncoding());
@ -561,14 +561,14 @@ public class RestfulServerUtils {
if (theAddContentLocationHeader && fullId != null) { if (theAddContentLocationHeader && fullId != null) {
if (theServer.getFhirContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { if (theServer.getFhirContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
restUtil.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue()); response.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue());
} }
restUtil.addHeader(Constants.HEADER_LOCATION, fullId.getValue()); response.addHeader(Constants.HEADER_LOCATION, fullId.getValue());
} }
if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) { if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) {
if (fullId != null && fullId.hasVersionIdPart()) { if (fullId != null && fullId.hasVersionIdPart()) {
restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + fullId.getVersionIdPart() + '"'); response.addHeader(Constants.HEADER_ETAG, "W/\"" + fullId.getVersionIdPart() + '"');
} }
} }
@ -582,9 +582,9 @@ public class RestfulServerUtils {
} }
// Force binary resources to download - This is a security measure to prevent // Force binary resources to download - This is a security measure to prevent
// malicious images or HTML blocks being served up as content. // malicious images or HTML blocks being served up as content.
restUtil.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
return restUtil.sendAttachmentResponse(bin, theStausCode, contentType); return response.sendAttachmentResponse(bin, theStausCode, contentType);
} }
// Ok, we're not serving a binary resource, so apply default encoding // Ok, we're not serving a binary resource, so apply default encoding
@ -615,7 +615,7 @@ public class RestfulServerUtils {
lastUpdated = extractLastUpdatedFromResource(theResource); lastUpdated = extractLastUpdatedFromResource(theResource);
} }
if (lastUpdated != null && lastUpdated.isEmpty() == false) { if (lastUpdated != null && lastUpdated.isEmpty() == false) {
restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue())); response.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue()));
} }
/* /*
@ -631,7 +631,7 @@ public class RestfulServerUtils {
} }
String charset = Constants.CHARSET_NAME_UTF8; String charset = Constants.CHARSET_NAME_UTF8;
Writer writer = restUtil.getResponseWriter(theStausCode, theStatusMessage, contentType, charset, respondGzip); Writer writer = response.getResponseWriter(theStausCode, theStatusMessage, contentType, charset, respondGzip);
if (theResource == null) { if (theResource == null) {
// No response is being returned // No response is being returned
} else if (encodingDomainResourceAsText && theResource instanceof IResource) { } else if (encodingDomainResourceAsText && theResource instanceof IResource) {
@ -641,7 +641,7 @@ public class RestfulServerUtils {
parser.encodeResourceToWriter(theResource, writer); parser.encodeResourceToWriter(theResource, writer);
} }
//FIXME resource leak //FIXME resource leak
return restUtil.sendWriterResponse(theStausCode, contentType, charset, writer); return response.sendWriterResponse(theStausCode, contentType, charset, writer);
} }
public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) {

View File

@ -0,0 +1,9 @@
package ca.uhn.fhir.rest.server.graphql;
import ca.uhn.fhir.rest.annotation.Operation;
public class GraphQLProviderR4 {
}

View File

@ -92,25 +92,15 @@ public abstract class BaseMethodBinding<T> {
return parser; return parser;
} }
protected IParser createAppropriateParserForParsingServerRequest(RequestDetails theRequest) { protected Object[] createMethodParams(RequestDetails theRequest) {
String contentTypeHeader = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); Object[] params = new Object[getParameters().size()];
EncodingEnum encoding; for (int i = 0; i < getParameters().size(); i++) {
if (isBlank(contentTypeHeader)) { IParameter param = getParameters().get(i);
encoding = EncodingEnum.XML; if (param != null) {
} else { params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
int semicolon = contentTypeHeader.indexOf(';');
if (semicolon != -1) {
contentTypeHeader = contentTypeHeader.substring(0, semicolon);
} }
encoding = EncodingEnum.forContentType(contentTypeHeader);
} }
return params;
if (encoding == null) {
throw new InvalidRequestException("Request contins non-FHIR conent-type header value: " + contentTypeHeader);
}
IParser parser = encoding.newParser(getContext());
return parser;
} }
protected Object[] createParametersForServerRequest(RequestDetails theRequest) { protected Object[] createParametersForServerRequest(RequestDetails theRequest) {
@ -345,16 +335,21 @@ public abstract class BaseMethodBinding<T> {
Operation operation = theMethod.getAnnotation(Operation.class); Operation operation = theMethod.getAnnotation(Operation.class);
GetPage getPage = theMethod.getAnnotation(GetPage.class); GetPage getPage = theMethod.getAnnotation(GetPage.class);
Patch patch = theMethod.getAnnotation(Patch.class); Patch patch = theMethod.getAnnotation(Patch.class);
GraphQL graphQL = theMethod.getAnnotation(GraphQL.class);
// ** if you add another annotation above, also add it to the next line: // ** if you add another annotation above, also add it to the next line:
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch)) { if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch, graphQL)) {
return null; return null;
} }
if (getPage != null) { if (getPage != null) {
return new PageMethodBinding(theContext, theMethod); return new PageMethodBinding(theContext, theMethod);
} }
if (graphQL != null) {
return new GraphQLMethodBinding(theMethod, theContext, theProvider);
}
Class<? extends IBaseResource> returnType; Class<? extends IBaseResource> returnType;
Class<? extends IBaseResource> returnTypeFromRp = null; Class<? extends IBaseResource> returnTypeFromRp = null;

View File

@ -170,14 +170,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
} }
public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
// Method params Object[] params = createMethodParams(theRequest);
Object[] params = new Object[getParameters().size()];
for (int i = 0; i < getParameters().size(); i++) {
IParameter param = getParameters().get(i);
if (param != null) {
params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
}
}
Object resultObj = invokeServer(theServer, theRequest, params); Object resultObj = invokeServer(theServer, theRequest, params);

View File

@ -0,0 +1,60 @@
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
public class GraphQLMethodBinding extends BaseMethodBinding<String> {
public GraphQLMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(theMethod, theContext, theProvider);
}
@Override
public String getResourceName() {
return null;
}
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.GRAPHQL_REQUEST;
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if ("graphql".equals(theRequest.getOperation())) {
return true;
}
return false;
}
@Override
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
Object[] methodParams = createMethodParams(theRequest);
Object response = invokeServerMethod(theServer, theRequest, methodParams);
int statusCode = Constants.STATUS_HTTP_200_OK;
String statusMessage = Constants.HTTP_STATUS_NAMES.get(statusCode);
String contentType = Constants.CT_JSON;
String charset = Constants.CHARSET_NAME_UTF8;
boolean respondGzip = theRequest.isRespondGzip();
Writer writer = theRequest.getResponse().getResponseWriter(statusCode, statusMessage, contentType, charset, respondGzip);
String responseString = (String) response;
writer.write(responseString);
writer.close();
return null;
}
}

View File

@ -0,0 +1,64 @@
package ca.uhn.fhir.rest.server.method;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Method;
import java.util.Collection;
public class GraphQLQueryParameter implements IParameter {
private Class<?> myType;
@Override
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
String[] queryParams = theRequest.getParameters().get(Constants.PARAM_GRAPHQL_QUERY);
String retVal = null;
if (queryParams != null) {
if (queryParams.length > 0) {
retVal = queryParams[0];
}
}
return retVal;
}
@Override
public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
if (theOuterCollectionType != null) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" +theMethod.getDeclaringClass().getCanonicalName()+ "' is annotated with @" + Count.class.getName() + " but can not be of collection type");
}
if (!String.class.equals(theParameterType)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" +theMethod.getDeclaringClass().getCanonicalName()+ "' is annotated with @" + Count.class.getName() + " but type '" + theParameterType + "' is an invalid type, must be one of Integer or IntegerType");
}
myType = theParameterType;
}
}

View File

@ -163,6 +163,8 @@ public class MethodUtil {
((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
} else if (nextAnnotation instanceof Count) { } else if (nextAnnotation instanceof Count) {
param = new CountParameter(); param = new CountParameter();
} else if (nextAnnotation instanceof GraphQLQuery) {
param = new GraphQLQueryParameter();
} else if (nextAnnotation instanceof Sort) { } else if (nextAnnotation instanceof Sort) {
param = new SortParameter(theContext); param = new SortParameter(theContext);
} else if (nextAnnotation instanceof TransactionParam) { } else if (nextAnnotation instanceof TransactionParam) {

View File

@ -1,11 +1,10 @@
package org.hl7.fhir.r4.hapi.ctx; package org.hl7.fhir.r4.hapi.ctx;
import org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider;
import org.hl7.fhir.r4.hapi.rest.server.ServerProfileProvider;
import ca.uhn.fhir.rest.api.server.IFhirVersionServer; import ca.uhn.fhir.rest.api.server.IFhirVersionServer;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider;
import org.hl7.fhir.r4.hapi.rest.server.ServerProfileProvider;
public class FhirServerR4 implements IFhirVersionServer { public class FhirServerR4 implements IFhirVersionServer {
@Override @Override

View File

@ -0,0 +1,126 @@
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.EncodingEnum;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
public class GraphQLR4ProviderTest {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static TokenAndListParam ourIdentifiers;
private static String ourLastMethod;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLR4ProviderTest.class);
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastMethod = null;
ourIdentifiers = null;
}
@Test
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar");
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("search", ourLastMethod);
assertEquals("foo", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem());
assertEquals("bar", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@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);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
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();
}
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) {
ourLastMethod = "search";
ourIdentifiers = theIdentifiers;
ArrayList<Patient> retVal = new ArrayList<Patient>();
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;
}
}
}

View File

@ -0,0 +1,124 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
public class GraphQLR4RawTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLR4RawTest.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort;
private static Server ourServer;
private static String ourNextRetVal;
private static IdType ourLastId;
private static String ourLastQuery;
private static int ourMethodCount;
@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);
MyProvider provider = new MyProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.registerProvider(provider);
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() {
ourNextRetVal = null;
ourLastId = null;
ourLastQuery = null;
ourMethodCount = 0;
}
@Test
public void testGraphInstance() throws Exception {
ourNextRetVal = "{\"foo\"}";
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$graphql?query=" + UrlUtil.escape("{name{family,given}}"));
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("{\"foo\"}", responseContent);
assertEquals("application/json", status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
assertEquals("Patient/123", ourLastId.getValue());
assertEquals("{name{family,given}}", ourLastQuery);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
public static class MyProvider {
@GraphQL
public String process(@IdParam IdType theId, @GraphQLQuery String theQuery) {
ourMethodCount++;
ourLastId = theId;
ourLastQuery = theQuery;
return ourNextRetVal;
}
}
}

View File

@ -0,0 +1,131 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.exceptions.FHIRException;
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.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;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GraphQLEngineTest {
private static HapiWorkerContext ourWorkerCtx;
private static FhirContext ourCtx;
private org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GraphQLEngineTest.class);
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
ourWorkerCtx = new HapiWorkerContext(ourCtx, new DefaultProfileValidationSupport());
}
@Test
public void testGraphSimple() throws EGraphQLException, EGraphEngine, IOException, FHIRException {
Observation obs = createObservation();
GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx);
engine.setFocus(obs);
engine.setGraphQL(Parser.parse("{valueQuantity{value,unit}}"));
engine.execute();
ObjectValue output = engine.getOutput();
StringBuilder outputBuilder = new StringBuilder();
output.write(outputBuilder, 0, "\n");
String expected = "{\n" +
" \"valueQuantity\":{\n" +
" \"value\":123,\n" +
" \"unit\":\"cm\"\n" +
" }\n" +
"}";
assertEquals(expected, outputBuilder.toString());
}
private Observation createObservation() {
Observation obs = new Observation();
obs.setId("http://foo.com/Patient/PATA");
obs.setValue(new Quantity().setValue(123).setUnit("cm"));
obs.setSubject(new Reference("Patient/123"));
return obs;
}
@Test
public void testReferences() throws EGraphQLException, EGraphEngine, IOException, FHIRException {
String graph = " { \n" +
" id\n" +
" subject { \n" +
" reference\n" +
" resource(type : Patient) { birthDate }\n" +
" resource(type : Practioner) { practitionerRole { speciality } }\n" +
" } \n" +
" code {coding {system code} }\n" +
" }\n" +
" ";
GraphQLEngine engine = new GraphQLEngine(ourWorkerCtx);
engine.setFocus(createObservation());
engine.setGraphQL(Parser.parse(graph));
engine.setServices(createStorageServices());
engine.execute();
ObjectValue output = engine.getOutput();
StringBuilder outputBuilder = new StringBuilder();
output.write(outputBuilder, 0, "\n");
String expected = "{\n" +
" \"id\":\"http://foo.com/Patient/PATA\",\n" +
" \"subject\":{\n" +
" \"reference\":\"Patient/123\",\n" +
" \"resource\":{\n" +
" \"birthDate\":\"2011-02-22\"\n" +
" }\n" +
" }\n" +
"}";
assertEquals(expected, outputBuilder.toString());
}
private GraphQLEngine.IGraphQLStorageServices createStorageServices() throws FHIRException {
GraphQLEngine.IGraphQLStorageServices retVal = mock(GraphQLEngine.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 {
Object appInfo = invocation.getArguments()[0];
Resource context = (Resource) invocation.getArguments()[1];
Reference reference = (Reference) invocation.getArguments()[2];
ourLog.info("AppInfo: {} / Context: {} / Reference: {}", appInfo, context.getId(), reference.getReference());
if (reference.getReference().equalsIgnoreCase("Patient/123")) {
Patient p = new Patient();
p.getBirthDateElement().setValueAsString("2011-02-22");
return new GraphQLEngine.IGraphQLStorageServices.ReferenceResolution(context, p);
}
ourLog.info("Not found!");
return null;
}
});
return retVal;
}
}

View File

@ -1,65 +1,79 @@
package org.hl7.fhir.utilities.graphql; package org.hl7.fhir.utilities.graphql;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
public class ObjectValue extends Value { public class ObjectValue extends Value {
private List<Argument> fields = new ArrayList<Argument>(); private List<Argument> fields = new ArrayList<Argument>();
public ObjectValue() { public ObjectValue() {
super(); super();
} }
public ObjectValue(JsonObject json) throws EGraphQLException { public ObjectValue(JsonObject json) throws EGraphQLException {
super(); super();
for (Entry<String, JsonElement> n : json.entrySet()) for (Entry<String, JsonElement> n : json.entrySet())
fields.add(new Argument(n.getKey(), n.getValue())); fields.add(new Argument(n.getKey(), n.getValue()));
} }
public List<Argument> getFields() { public List<Argument> getFields() {
return fields; return fields;
} }
public Argument addField(String name, boolean isList) { public Argument addField(String name, boolean isList) {
Argument result = null; Argument result = null;
for (Argument t : fields) for (Argument t : fields)
if ((t.name.equals(name))) if ((t.name.equals(name)))
result = t; result = t;
if (result == null) { if (result == null) {
result = new Argument(); result = new Argument();
result.setName(name); result.setName(name);
result.setList(isList); result.setList(isList);
fields.add(result); fields.add(result);
} else } else
result.list = true; result.list = true;
return result; return result;
} }
public void write(StringBuilder b, int indent) throws EGraphQLException, EGraphEngine { /**
b.append("{"); * Write the output using the system default line separator (as defined in {@link System#lineSeparator}
int ni = indent; * @param b The StringBuilder to populate
String s = ""; * @param indent The indent level, or <code>-1</code> for no indent
String se = ""; */
if ((ni > -1)) public void write(StringBuilder b, int indent) throws EGraphQLException, EGraphEngine {
{ write(b, indent, System.lineSeparator());
se = "\r\n"+Utilities.padLeft("",' ', ni*2); }
ni++;
s = "\r\n"+Utilities.padLeft("",' ', ni*2); /**
} * Write the output using the system default line separator (as defined in {@link System#lineSeparator}
boolean first = true; * @param b The StringBuilder to populate
for (Argument a : fields) { * @param indent The indent level, or <code>-1</code> for no indent
if (first) first = false; else b.append(","); * @param lineSeparator The line separator
b.append(s); */
a.write(b, ni); public void write(StringBuilder b, Integer indent, String lineSeparator) throws EGraphQLException, EGraphEngine {
} b.append("{");
b.append(se); String s = "";
b.append("}"); String se = "";
if ((indent > -1))
} {
} se = lineSeparator + Utilities.padLeft("",' ', indent*2);
indent++;
s = lineSeparator + Utilities.padLeft("",' ', indent*2);
}
boolean first = true;
for (Argument a : fields) {
if (first) first = false; else b.append(",");
b.append(s);
a.write(b, indent);
}
b.append(se);
b.append("}");
}
}