Add FHIRPath evaluation interceptor (#1769)

* Add FHIRPath evaluation interceptor

* Add changelog
This commit is contained in:
James Agnew 2020-03-22 12:57:24 -04:00 committed by GitHub
parent 75402e38f6
commit c2ae5a8326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 469 additions and 62 deletions

View File

@ -3,7 +3,7 @@ package ca.uhn.fhir.context;
import ca.uhn.fhir.context.api.AddProfileTagEnum;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.IFhirVersion;
@ -625,12 +625,21 @@ public class FhirContext {
}
/**
* Creates a new FluentPath engine which can be used to exvaluate
* @since 2.2
* @deprecated Deprecated in HAPI FHIR 5.0.0. Use {@link #newFhirPath()} instead.
*/
@Deprecated
public IFhirPath newFluentPath() {
return newFhirPath();
}
/**
* Creates a new FhirPath engine which can be used to evaluate
* path expressions over FHIR resources. Note that this engine will use the
* {@link IValidationSupport context validation support} module which is
* configured on the context at the time this method is called.
* <p>
* In other words, call {@link #setValidationSupport(IValidationSupport)} before
* In other words, you may wish to call {@link #setValidationSupport(IValidationSupport)} before
* calling {@link #newFluentPath()}
* </p>
* <p>
@ -640,9 +649,9 @@ public class FhirContext {
* {@link UnsupportedOperationException}
* </p>
*
* @since 2.2
* @since 5.0.0
*/
public IFluentPath newFluentPath() {
public IFhirPath newFhirPath() {
return myVersion.createFluentPathExecutor(this);
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.fluentpath;
package ca.uhn.fhir.fhirpath;
/*
* #%L
@ -23,18 +23,18 @@ package ca.uhn.fhir.fluentpath;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
/**
* This exception is thrown if a FluentPath expression can not be executed successfully
* This exception is thrown if a FHIRPath expression can not be executed successfully
* for any reason
*/
public class FluentPathExecutionException extends InternalErrorException {
public class FhirPathExecutionException extends InternalErrorException {
private static final long serialVersionUID = 1L;
public FluentPathExecutionException(Throwable theCause) {
public FhirPathExecutionException(Throwable theCause) {
super(theCause);
}
public FluentPathExecutionException(String theMessage) {
public FhirPathExecutionException(String theMessage) {
super(theMessage);
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.fluentpath;
package ca.uhn.fhir.fhirpath;
/*
* #%L
@ -25,7 +25,7 @@ import java.util.Optional;
import org.hl7.fhir.instance.model.api.IBase;
public interface IFluentPath {
public interface IFhirPath {
/**
* Apply the given FluentPath expression against the given input and return

View File

@ -23,10 +23,10 @@ package ca.uhn.fhir.model.api;
import java.io.InputStream;
import java.util.Date;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
/**
@ -38,7 +38,7 @@ import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
*/
public interface IFhirVersion {
IFluentPath createFluentPathExecutor(FhirContext theFhirContext);
IFhirPath createFluentPathExecutor(FhirContext theFhirContext);
IBaseResource generateProfile(RuntimeResourceDefinition theRuntimeResourceDefinition, String theServerBase);

View File

@ -24,7 +24,7 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBase;
@ -120,7 +120,7 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
return Collections.singletonList(theResource);
}
IFluentPath fhirPath = theFhirContext.newFluentPath();
IFhirPath fhirPath = theFhirContext.newFluentPath();
return fhirPath.evaluate(theResource, theContextPath, IBase.class);
}

View File

@ -259,6 +259,7 @@ public class Constants {
* </p>
*/
public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source";
public static final String PARAM_FHIRPATH = "_fhirpath";
static {
CHARSET_UTF8 = StandardCharsets.UTF_8;

View File

@ -238,7 +238,7 @@ public class ParametersUtil {
addPart(theContext, theParameter, theName, coding);
}
private static void addPart(FhirContext theContext, IBase theParameter, String theName, IBase theValue) {
public static void addPart(FhirContext theContext, IBase theParameter, String theName, IBase theValue) {
BaseRuntimeElementCompositeDefinition<?> def = (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theParameter.getClass());
BaseRuntimeChildDefinition partChild = def.getChildByName("part");
@ -252,4 +252,19 @@ public class ParametersUtil {
partChildElem.getChildByName("value[x]").getMutator().addValue(part, theValue);
}
public static void addPartResource(FhirContext theContext, IBase theParameter, String theName, IBaseResource theValue) {
BaseRuntimeElementCompositeDefinition<?> def = (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theParameter.getClass());
BaseRuntimeChildDefinition partChild = def.getChildByName("part");
BaseRuntimeElementCompositeDefinition<?> partChildElem = (BaseRuntimeElementCompositeDefinition<?>) partChild.getChildByName("part");
IBase part = partChildElem.newInstance();
partChild.getMutator().addValue(theParameter, part);
IPrimitiveType<String> name = (IPrimitiveType<String>) theContext.getElementDefinition("string").newInstance();
name.setValue(theName);
partChildElem.getChildByName("name").getMutator().addValue(part, name);
partChildElem.getChildByName("resource").getMutator().addValue(part, theValue);
}
}

View File

@ -32,11 +32,11 @@ import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import java.util.Arrays;
@SuppressWarnings("serial")
@SuppressWarnings({"serial", "RedundantThrows", "InnerClassMayBeStatic"})
public class ServletExamples {
// START SNIPPET: loggingInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class RestfulServerWithLogging extends RestfulServer {
@Override
@ -121,6 +121,24 @@ public class ServletExamples {
}
// END SNIPPET: exceptionInterceptor
// START SNIPPET: fhirPathInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class RestfulServerWithFhirPath extends RestfulServer {
@Override
protected void initialize() throws ServletException {
// ... define your resource providers here ...
// Now register the interceptor
FhirPathFilterInterceptor interceptor = new FhirPathFilterInterceptor();
registerInterceptor(interceptor);
}
}
// END SNIPPET: fhirPathInterceptor
// START SNIPPET: responseHighlighterInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class RestfulServerWithResponseHighlighter extends RestfulServer {

View File

@ -0,0 +1,6 @@
---
type: add
issue: 1769
title: A new built-in server interceptor called FhirPathFilterInterceptor has been added. This interceptor
evaluates an arbitrary FHIRPath expression against the resource being returned and replaces the response
with a Parameters resource containing the results of the evaluation.

View File

@ -17,3 +17,11 @@
[Migrating to HAPI FHIR 5.x](/hapi-fhir/docs/validation/instance_validator.html#migrating-to-hapi-fhir-5x)
for details on how to account for this change in your code.
"
- item:
issue: "1769"
type: "change"
title: "**Breaking Change**:
The `IFluentPath` interface has been renamed to `IFhirPath`, and the `FhirContext#newFluentPath()` method
has been replaced with an equivalent `FhirContext.newFhirPath()`. The FhirPath expression language was initially
called FluentPath before being renamed, so this change brings HAPI FHIR inline with the correct naming.
"

View File

@ -50,6 +50,52 @@ The following example shows how to register the ExceptionHandlingInterceptor.
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|exceptionInterceptor}}
```
# Response Customizing: Evaluate FHIRPath
The FhirPathFilterInterceptor looks for a request URL parameter in the form `_fhirpath=(expression)` in all REST requests. If this parameter is found, the value is treated as a [FHIRPath](http://hl7.org/fhirpath/) expression. The response resource will be replaced with a [Parameters](hl7.org/fhir/parameters.html) resource containing the results of the given expression applied against the response resource.
* [FhirPathFilterInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.html)
* [FhirPathFilterInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.java)
The following example shows how to register the ExceptionHandlingInterceptor.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|exceptionInterceptor}}
```
An example URL to invoke this function is shown below:
```url
https://hapi.fhir.org/baseR4/Patient?_fhirpath=Bundle.entry.resource.as(Patient).name&_pretty=true
```
A sample response to this query is shown below:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "result",
"part": [ {
"name": "expression",
"valueString": "Bundle.entry.resource.as(Patient).name"
}, {
"name": "result",
"valueHumanName": {
"family": "Simpson",
"given": [ "Homer", "Jay" ]
}
}, {
"name": "result",
"valueHumanName": {
"family": "Simpson",
"given": [ "Grandpa" ]
}
} ]
} ]
}
```
# Validation: Request and Response Validation
HAPI FHIR provides a pair of interceptors that can be used to validate incoming requests received by the server, as well as outgoing responses generated by the server.

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.BanUnsupportedHttpMethodsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.FhirPathFilterInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhirtest.config.TestDstu2Config;
import ca.uhn.fhirtest.config.TestDstu3Config;
@ -198,6 +199,11 @@ public class TestRestfulServer extends RestfulServer {
CorsInterceptor corsInterceptor = new CorsInterceptor();
registerInterceptor(corsInterceptor);
/*
* Enable FHIRPath evaluation
*/
registerInterceptor(new FhirPathFilterInterceptor());
/*
* Enable version conversion
*/

View File

@ -0,0 +1,70 @@
package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.fhirpath.FhirPathExecutionException;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor looks for a URL parameter on requests called <code>_fhirpath</code> and
* replaces the resource being returned with a Parameters resource containing the results of
* the given FHIRPath expression evaluated against the resource that would otherwise
* have been returned.
*
* @see <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#response-customizing-evaluate-fhirpath">Interceptors - Response Customization: Evaluate FHIRPath</a>
* @since 5.0.0
*/
public class FhirPathFilterInterceptor {
@Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
public void preProcessOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails) {
IBaseResource responseResource = theResponseDetails.getResponseResource();
if (responseResource != null) {
String[] fhirPathParams = theRequestDetails.getParameters().get(Constants.PARAM_FHIRPATH);
if (fhirPathParams != null) {
FhirContext ctx = theRequestDetails.getFhirContext();
IBaseParameters responseParameters = ParametersUtil.newInstance(ctx);
for (String expression : fhirPathParams) {
if (isNotBlank(expression)) {
IBase resultPart = ParametersUtil.addParameterToParameters(ctx, responseParameters, "result");
ParametersUtil.addPartString(ctx, resultPart, "expression", expression);
IFhirPath fhirPath = ctx.newFhirPath();
List<IBase> outputs;
try {
outputs = fhirPath.evaluate(responseResource, expression, IBase.class);
} catch (FhirPathExecutionException e) {
throw new InvalidRequestException("Error parsing FHIRPath expression: " + e.getMessage());
}
for (IBase nextOutput : outputs) {
if (nextOutput instanceof IBaseResource) {
ParametersUtil.addPartResource(ctx, resultPart, "result", (IBaseResource) nextOutput);
} else {
ParametersUtil.addPart(ctx, resultPart, "result", nextOutput);
}
}
}
}
theResponseDetails.setResponseResource(responseParameters);
}
}
}
}

View File

@ -60,7 +60,7 @@ import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeResourceBlockDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.support.IContextValidationSupport;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFluentPath;
import ca.uhn.fhir.model.api.ICompositeDatatype;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;

View File

@ -30,7 +30,7 @@ import org.hl7.fhir.dstu2016may.model.*;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
@ -41,7 +41,7 @@ public class FhirDstu2_1 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
throw new UnsupportedOperationException("FluentPath is not supported in DSTU2 contexts");
}

View File

@ -23,11 +23,11 @@ package ca.uhn.fhir.model.dstu2;
import java.io.InputStream;
import java.util.Date;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.base.composite.*;
import ca.uhn.fhir.model.dstu2.composite.*;
@ -42,7 +42,7 @@ public class FhirDstu2 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
throw new UnsupportedOperationException("FluentPath is not supported in DSTU2 contexts");
}

View File

@ -24,13 +24,13 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
import ca.uhn.fhir.util.ReflectionUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.hapi.fluentpath.FluentPathDstu3;
import org.hl7.fhir.dstu3.hapi.fluentpath.FhirPathDstu3;
import org.hl7.fhir.dstu3.hapi.rest.server.Dstu3BundleFactory;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.*;
@ -44,8 +44,8 @@ public class FhirDstu3 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
return new FluentPathDstu3(theFhirContext);
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
return new FhirPathDstu3(theFhirContext);
}
@Override

View File

@ -2,8 +2,8 @@ package org.hl7.fhir.dstu3.hapi.fluentpath;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.fluentpath.FluentPathExecutionException;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.FhirPathExecutionException;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.dstu3.model.Base;
import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
@ -13,11 +13,11 @@ import org.hl7.fhir.instance.model.api.IBase;
import java.util.List;
import java.util.Optional;
public class FluentPathDstu3 implements IFluentPath {
public class FhirPathDstu3 implements IFhirPath {
private FHIRPathEngine myEngine;
public FluentPathDstu3(FhirContext theCtx) {
public FhirPathDstu3(FhirContext theCtx) {
IValidationSupport validationSupport = theCtx.getValidationSupport();
myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport));
}
@ -29,12 +29,12 @@ public class FluentPathDstu3 implements IFluentPath {
try {
result = myEngine.evaluate((Base)theInput, thePath);
} catch (FHIRException e) {
throw new FluentPathExecutionException(e);
throw new FhirPathExecutionException(e);
}
for (Base next : result) {
if (!theReturnType.isAssignableFrom(next.getClass())) {
throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
throw new FhirPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
}
}

View File

@ -24,12 +24,12 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu2.model.*;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
@ -41,7 +41,7 @@ public class FhirDstu2Hl7Org implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
throw new UnsupportedOperationException("FluentPath is not supported in DSTU2 contexts");
}

View File

@ -26,12 +26,12 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.hapi.fluentpath.FluentPathR4;
import org.hl7.fhir.r4.hapi.fluentpath.FhirPathR4;
import org.hl7.fhir.r4.hapi.rest.server.R4BundleFactory;
import org.hl7.fhir.r4.model.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
@ -42,8 +42,8 @@ public class FhirR4 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
return new FluentPathR4(theFhirContext);
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
return new FhirPathR4(theFhirContext);
}
@Override

View File

@ -2,8 +2,8 @@ package org.hl7.fhir.r4.hapi.fluentpath;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.fluentpath.FluentPathExecutionException;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.FhirPathExecutionException;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
@ -13,11 +13,11 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine;
import java.util.List;
import java.util.Optional;
public class FluentPathR4 implements IFluentPath {
public class FhirPathR4 implements IFhirPath {
private FHIRPathEngine myEngine;
public FluentPathR4(FhirContext theCtx) {
public FhirPathR4(FhirContext theCtx) {
IValidationSupport validationSupport = theCtx.getValidationSupport();
myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport));
}
@ -29,12 +29,12 @@ public class FluentPathR4 implements IFluentPath {
try {
result = myEngine.evaluate((Base) theInput, thePath);
} catch (FHIRException e) {
throw new FluentPathExecutionException(e);
throw new FhirPathExecutionException(e);
}
for (Base next : result) {
if (!theReturnType.isAssignableFrom(next.getClass())) {
throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
throw new FhirPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
}
}

View File

@ -0,0 +1,170 @@
package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.test.utilities.HttpClientRule;
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderRule;
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
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.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class FhirPathFilterInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(FhirPathFilterInterceptorTest.class);
@ClassRule
public static HttpClientRule ourClientRule = new HttpClientRule();
private static FhirContext ourCtx = FhirContext.forR4();
@ClassRule
public static RestfulServerRule ourServerRule = new RestfulServerRule(ourCtx);
@ClassRule
public static HashMapResourceProviderRule<Patient> ourProviderRule = new HashMapResourceProviderRule<>(ourServerRule, Patient.class);
private IGenericClient myClient;
private String myBaseUrl;
private CloseableHttpClient myHttpClient;
private IIdType myPatientId;
@Before
public void before() {
ourProviderRule.clear();
ourServerRule.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
ourServerRule.getRestfulServer().getInterceptorService().registerInterceptor(new FhirPathFilterInterceptor());
myClient = ourServerRule.getFhirClient();
myBaseUrl = "http://localhost:" + ourServerRule.getPort();
myHttpClient = ourClientRule.getClient();
}
@Test
public void testUnfilteredResponse() throws IOException {
createPatient();
HttpGet request = new HttpGet(myPatientId.getValue());
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString("\"system\": \"http://identifiers/1\""));
assertThat(responseText, containsString("\"given\": [ \"Homer\", \"Jay\" ]"));
}
}
@Test
public void testUnfilteredResponse_WithResponseHighlightingInterceptor() throws IOException {
ourServerRule.getRestfulServer().registerInterceptor(new ResponseHighlighterInterceptor());
createPatient();
HttpGet request = new HttpGet(myPatientId.getValue() + "?_format=" + Constants.FORMATS_HTML_JSON);
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString("<span class='hlTagName'>&quot;system&quot;</span>: <span class='hlQuot'>&quot;http://identifiers/1&quot;"));
assertThat(responseText, containsString("<span class='hlTagName'>&quot;given&quot;</span>: <span class='hlControl'>[</span> <span class='hlTagName'>&quot;Homer&quot;</span><span class='hlControl'>,</span> <span class='hlTagName'>&quot;Jay&quot;</span> ]</div>"));
}
}
@Test
public void testFilteredResponse() throws IOException {
createPatient();
HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.identifier&_pretty=true");
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString("\"system\": \"http://identifiers/1\""));
assertThat(responseText, not(containsString("\"given\": [ \"Homer\", \"Jay\" ]")));
}
}
@Test
public void testFilteredResponse_ExpressionReturnsResource() throws IOException {
createPatient();
HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient&_pretty=true");
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString("\"resource\": {"));
assertThat(responseText, containsString("\"system\": \"http://identifiers/1\""));
assertThat(responseText, containsString("\"given\": [ \"Homer\", \"Jay\" ]"));
}
}
@Test
public void testFilteredResponse_ExpressionIsInvalid() throws IOException {
createPatient();
HttpGet request = new HttpGet(myPatientId + "?_fhirpath=" + UrlUtil.escapeUrlParam("***"));
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertEquals(400, response.getStatusLine().getStatusCode());
assertThat(responseText, containsString("Error parsing FHIRPath expression: Error performing *: left operand has more than one value"));
}
}
@Test
public void testFilteredResponseBundle() throws IOException {
createPatient();
HttpGet request = new HttpGet(myBaseUrl + "/Patient?_fhirpath=Bundle.entry.resource.as(Patient).name&_pretty=true");
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString(
" \"valueHumanName\": {\n" +
" \"family\": \"Simpson\",\n" +
" \"given\": [ \"Homer\", \"Jay\" ]\n" +
" }"
));
}
}
@Test
public void testFilteredResponse_WithResponseHighlightingInterceptor() throws IOException {
ourServerRule.getRestfulServer().registerInterceptor(new ResponseHighlighterInterceptor());
createPatient();
HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.identifier&_format=" + Constants.FORMATS_HTML_JSON);
try (CloseableHttpResponse response = myHttpClient.execute(request)) {
String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response:\n{}", responseText);
assertThat(responseText, containsString("<span class='hlTagName'>&quot;system&quot;</span>: <span class='hlQuot'>&quot;http://identifiers/1&quot;"));
assertThat(responseText, not(containsString("<span class='hlTagName'>&quot;given&quot;</span>: <span class='hlControl'>[</span> <span class='hlTagName'>&quot;Homer&quot;</span><span class='hlControl'>,</span> <span class='hlTagName'>&quot;Jay&quot;</span> ]</div>")));
}
}
private void createPatient() {
Patient p = new Patient();
p.setActive(true);
p.addIdentifier().setSystem("http://identifiers/1").setValue("value-1");
p.addIdentifier().setSystem("http://identifiers/2").setValue("value-2");
p.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay");
p.addName().setFamily("Simpson").addGiven("Grandpa");
myPatientId = myClient.create().resource(p).execute().getId().withServerBase(myBaseUrl, "Patient");
}
}

View File

@ -24,7 +24,7 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.model.api.IFhirVersion;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
@ -52,7 +52,7 @@ public class FhirR5 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
return new FhirPathR5(theFhirContext);
}

View File

@ -2,8 +2,8 @@ package org.hl7.fhir.r5.hapi.fhirpath;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.fluentpath.FluentPathExecutionException;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.fhirpath.FhirPathExecutionException;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
@ -13,7 +13,7 @@ import org.hl7.fhir.r5.utils.FHIRPathEngine;
import java.util.List;
import java.util.Optional;
public class FhirPathR5 implements IFluentPath {
public class FhirPathR5 implements IFhirPath {
private FHIRPathEngine myEngine;
@ -29,12 +29,12 @@ public class FhirPathR5 implements IFluentPath {
try {
result = myEngine.evaluate((Base) theInput, thePath);
} catch (FHIRException e) {
throw new FluentPathExecutionException(e);
throw new FhirPathExecutionException(e);
}
for (Base next : result) {
if (!theReturnType.isAssignableFrom(next.getClass())) {
throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
throw new FhirPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName());
}
}

View File

@ -0,0 +1,58 @@
package ca.uhn.fhir.test.utilities;
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2020 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 org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class HttpClientRule implements TestRule {
private CloseableHttpClient myClient;
@Override
public Statement apply(Statement theBase, Description theDescription) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
startClient();
theBase.evaluate();
stopClient();
}
};
}
private void stopClient() throws Exception {
myClient.close();
}
private void startClient() {
myClient = HttpClientBuilder
.create()
.build();
}
public CloseableHttpClient getClient() {
return myClient;
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.fluentpath;
package ca.uhn.fhir.fhirpath;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.TestUtil;
@ -21,7 +21,7 @@ public class FluentPathTest {
p.addName().setFamily("N1F1").addGiven("N1G1").addGiven("N1G2");
p.addName().setFamily("N2F1").addGiven("N2G1").addGiven("N2G2");
IFluentPath fp = ourCtx.newFluentPath();
IFhirPath fp = ourCtx.newFluentPath();
List<HumanName> names = fp.evaluate(p, "Patient.name", HumanName.class);
assertEquals(2, names.size());
assertEquals("N1F1", names.get(0).getFamily());
@ -36,7 +36,7 @@ public class FluentPathTest {
p.addName().setFamily("N1F1").addGiven("N1G1").addGiven("N1G2");
p.addName().setFamily("N2F1").addGiven("N2G1").addGiven("N2G2");
IFluentPath fp = ourCtx.newFluentPath();
IFhirPath fp = ourCtx.newFluentPath();
List<HumanName> names = fp.evaluate(p, "Patient.nameFOO", HumanName.class);
assertEquals(0, names.size());
}
@ -47,10 +47,10 @@ public class FluentPathTest {
p.addName().setFamily("N1F1").addGiven("N1G1").addGiven("N1G2");
p.addName().setFamily("N2F1").addGiven("N2G1").addGiven("N2G2");
IFluentPath fp = ourCtx.newFluentPath();
IFhirPath fp = ourCtx.newFluentPath();
try {
fp.evaluate(p, "Patient....nameFOO", HumanName.class);
} catch (FluentPathExecutionException e) {
} catch (FhirPathExecutionException e) {
assertThat(e.getMessage(), containsString("termination at unexpected token"));
}
}
@ -61,10 +61,10 @@ public class FluentPathTest {
p.addName().setFamily("N1F1").addGiven("N1G1").addGiven("N1G2");
p.addName().setFamily("N2F1").addGiven("N2G1").addGiven("N2G2");
IFluentPath fp = ourCtx.newFluentPath();
IFhirPath fp = ourCtx.newFluentPath();
try {
fp.evaluate(p, "Patient.name", StringType.class);
} catch (FluentPathExecutionException e) {
} catch (FhirPathExecutionException e) {
assertEquals("FluentPath expression \"Patient.name\" returned unexpected type HumanName - Expected org.hl7.fhir.dstu3.model.StringType", e.getMessage());
}
}

View File

@ -23,11 +23,11 @@ package ca.uhn.fhir.model.dstu2;
import java.io.InputStream;
import java.util.Date;
import ca.uhn.fhir.fhirpath.IFhirPath;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.base.composite.*;
import ca.uhn.fhir.model.dstu2.composite.*;
@ -41,7 +41,7 @@ public class FhirDstu2 implements IFhirVersion {
private String myId;
@Override
public IFluentPath createFluentPathExecutor(FhirContext theFhirContext) {
public IFhirPath createFluentPathExecutor(FhirContext theFhirContext) {
throw new UnsupportedOperationException("FluentPath is not supported in DSTU2 contexts");
}