Work on graph QL support
This commit is contained in:
parent
1a95ba3b65
commit
5413b276af
|
@ -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 {
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ca.uhn.fhir.rest.server.graphql;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.annotation.Operation;
|
||||||
|
|
||||||
|
public class GraphQLProviderR4 {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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,9 +335,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,6 +346,10 @@ public abstract class BaseMethodBinding<T> {
|
||||||
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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -41,22 +41,36 @@ public class ObjectValue extends Value {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the output using the system default line separator (as defined in {@link System#lineSeparator}
|
||||||
|
* @param b The StringBuilder to populate
|
||||||
|
* @param indent The indent level, or <code>-1</code> for no indent
|
||||||
|
*/
|
||||||
public void write(StringBuilder b, int indent) throws EGraphQLException, EGraphEngine {
|
public void write(StringBuilder b, int indent) throws EGraphQLException, EGraphEngine {
|
||||||
|
write(b, indent, System.lineSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the output using the system default line separator (as defined in {@link System#lineSeparator}
|
||||||
|
* @param b The StringBuilder to populate
|
||||||
|
* @param indent The indent level, or <code>-1</code> for no indent
|
||||||
|
* @param lineSeparator The line separator
|
||||||
|
*/
|
||||||
|
public void write(StringBuilder b, Integer indent, String lineSeparator) throws EGraphQLException, EGraphEngine {
|
||||||
b.append("{");
|
b.append("{");
|
||||||
int ni = indent;
|
|
||||||
String s = "";
|
String s = "";
|
||||||
String se = "";
|
String se = "";
|
||||||
if ((ni > -1))
|
if ((indent > -1))
|
||||||
{
|
{
|
||||||
se = "\r\n"+Utilities.padLeft("",' ', ni*2);
|
se = lineSeparator + Utilities.padLeft("",' ', indent*2);
|
||||||
ni++;
|
indent++;
|
||||||
s = "\r\n"+Utilities.padLeft("",' ', ni*2);
|
s = lineSeparator + Utilities.padLeft("",' ', indent*2);
|
||||||
}
|
}
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
for (Argument a : fields) {
|
for (Argument a : fields) {
|
||||||
if (first) first = false; else b.append(",");
|
if (first) first = false; else b.append(",");
|
||||||
b.append(s);
|
b.append(s);
|
||||||
a.write(b, ni);
|
a.write(b, indent);
|
||||||
}
|
}
|
||||||
b.append(se);
|
b.append(se);
|
||||||
b.append("}");
|
b.append("}");
|
||||||
|
|
Loading…
Reference in New Issue