Support GraphQL for R3/4/5 (#1424)

* Work on grpahql enhanbcements

* Add some more chars to the sanitizer function

* Add changelog
This commit is contained in:
James Agnew 2019-08-12 08:24:32 -04:00 committed by GitHub
parent 2999a292e6
commit 0c9e5ec1ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 401 additions and 298 deletions

View File

@ -163,7 +163,16 @@ public class UrlUtil {
if (theString != null) {
for (int i = 0; i < theString.length(); i++) {
char nextChar = theString.charAt(i);
if (nextChar == '<' || nextChar == '"') {
switch (nextChar) {
case '\'':
case '"':
case '<':
case '>':
case '\n':
case '\r':
return true;
}
if (nextChar < ' ') {
return true;
}
}
@ -348,7 +357,17 @@ public class UrlUtil {
/**
* This method specifically HTML-encodes the &quot; and
* &lt; characters in order to prevent injection attacks
* &lt; characters in order to prevent injection attacks.
*
* The following characters are escaped:
* <ul>
* <li>&apos;</li>
* <li>&quot;</li>
* <li>&lt;</li>
* <li>&gt;</li>
* <li>\n (newline)</li>
* </ul>
*
*/
public static String sanitizeUrlPart(CharSequence theString) {
if (theString == null) {
@ -364,6 +383,10 @@ public class UrlUtil {
char nextChar = theString.charAt(j);
switch (nextChar) {
/*
* NB: If you add a constant here, you also need to add it
* to isNeedsSanitization()!!
*/
case '\'':
buffer.append("&apos;");
break;
@ -373,8 +396,19 @@ public class UrlUtil {
case '<':
buffer.append("&lt;");
break;
case '>':
buffer.append("&gt;");
break;
case '\n':
buffer.append("&#10;");
break;
case '\r':
buffer.append("&#13;");
break;
default:
buffer.append(nextChar);
if (nextChar >= ' ') {
buffer.append(nextChar);
}
break;
}

View File

@ -59,4 +59,15 @@ public class UrlUtilTest {
}
@Test
public void testSanitize() {
assertEquals(" &apos; ", UrlUtil.sanitizeUrlPart(" ' "));
assertEquals(" &lt; ", UrlUtil.sanitizeUrlPart(" < "));
assertEquals(" &gt; ", UrlUtil.sanitizeUrlPart(" > "));
assertEquals(" &quot; ", UrlUtil.sanitizeUrlPart(" \" "));
assertEquals(" &#10; ", UrlUtil.sanitizeUrlPart(" \n "));
assertEquals(" &#13; ", UrlUtil.sanitizeUrlPart(" \r "));
assertEquals(" ", UrlUtil.sanitizeUrlPart(" \0 "));
}
}

View File

@ -24,10 +24,8 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
@ -23,6 +24,7 @@ import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
@ -109,6 +111,12 @@ public abstract class BaseConfig implements SchedulingConfigurer {
public abstract FhirContext fhirContext();
@Bean
@Lazy
public IGraphQLStorageServices graphqlStorageServices() {
return new JpaStorageServices();
}
@Bean
public ScheduledExecutorFactoryBean scheduledExecutorService() {
ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean();

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
@ -73,6 +74,12 @@ public class BaseDstu3Config extends BaseConfig {
return retVal;
}
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider() {
return new GraphQLProvider(fhirContextDstu3(), validationSupportChainDstu3(), graphqlStorageServices());
}
@Bean
public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
return new TransactionProcessorVersionAdapterDstu3();

View File

@ -21,12 +21,13 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR4;
import ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport;
import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.r5.utils.IResourceValidator;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -91,12 +92,6 @@ public class BaseR4Config extends BaseConfig {
return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), graphqlStorageServices());
}
@Bean
@Lazy
public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() {
return new JpaStorageServices();
}
@Bean(name = "myInstanceValidatorR4")
@Lazy
public IValidatorModule instanceValidatorR4() {

View File

@ -8,7 +8,7 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.dao.r5.TransactionProcessorVersionAdapterR5;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR5;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR5;
@ -21,11 +21,9 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR5;
import ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r5.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r5.hapi.rest.server.GraphQLProvider;
import org.hl7.fhir.r5.hapi.validation.CachingValidationSupport;
import org.hl7.fhir.r5.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.utils.GraphQLEngine;
import org.hl7.fhir.r5.utils.IResourceValidator;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
@ -85,17 +83,11 @@ public class BaseR5Config extends BaseConfig {
return new TransactionProcessor<>();
}
// @Bean(name = GRAPHQL_PROVIDER_NAME)
// @Lazy
// public GraphQLProvider graphQLProvider() {
// return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices());
// }
//
// @Bean
// @Lazy
// public GraphQLEngine.IGraphQLStorageServices graphqlStorageServices() {
// return new JpaStorageServices();
// }
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider() {
return new GraphQLProvider(fhirContextR5(), validationSupportChainR5(), graphqlStorageServices());
}
@Bean(name = "myInstanceValidatorR5")
@Lazy

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -33,22 +32,21 @@ import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.Value;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implements GraphQLEngine.IGraphQLStorageServices {
public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implements IGraphQLStorageServices {
private static final int MAX_SEARCH_SIZE = 500;
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType);
@ -57,13 +55,13 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
@Transactional(propagation = Propagation.NEVER)
@Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<Resource> theMatches) throws FHIRException {
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theType);
IFhirResourceDao<? extends IBaseResource> dao = getDao(typeDef.getImplementingClass());
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(500);
params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
for (Argument nextArgument : theSearchParams) {
@ -114,40 +112,37 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
size = response.preferredPageSize();
}
for (IBaseResource next : response.getResources(0, size)) {
theMatches.add((Resource) next);
}
theMatches.addAll(response.getResources(0, size));
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Resource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
public IBaseResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
IIdType refId = getContext().getVersion().newIdType();
refId.setValue(theType + "/" + theId);
return lookup(theAppInfo, refId);
}
private Resource lookup(Object theAppInfo, IIdType theRefId) {
private IBaseResource lookup(Object theAppInfo, IIdType theRefId) {
IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType());
RequestDetails requestDetails = (RequestDetails) theAppInfo;
return (Resource) dao.read(theRefId, requestDetails, false);
return dao.read(theRefId, requestDetails, false);
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
IdType refId = new IdType(theReference.getReference());
Resource outcome = lookup(theAppInfo, refId);
@Override
public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException {
IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement());
if (outcome == null) {
return null;
}
return null;
}
return new ReferenceResolution(theContext, outcome);
}
@Transactional(propagation = Propagation.NEVER)
@Override
public Bundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
throw new NotImplementedOperationException("Not yet able to handle this GraphQL request");
}
}

View File

@ -0,0 +1,165 @@
package ca.uhn.fhir.jpa.provider;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 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.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.IContextValidationSupport;
import ca.uhn.fhir.rest.annotation.GraphQL;
import ca.uhn.fhir.rest.annotation.GraphQLQuery;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.utilities.graphql.IGraphQLEngine;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.function.Supplier;
public class GraphQLProvider {
private final Supplier<IGraphQLEngine> engineFactory;
private final IGraphQLStorageServices myStorageServices;
private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class);
/**
* Constructor which uses a default context and validation support object
*
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(IGraphQLStorageServices theStorageServices) {
this(FhirContext.forR4(), null, theStorageServices);
}
/**
* Constructor which uses the given worker context
*
* @param theFhirContext The HAPI FHIR Context object
* @param theValidationSupport The HAPI Validation Support object, or null
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(@Nonnull FhirContext theFhirContext, @Nullable IContextValidationSupport theValidationSupport, @Nonnull IGraphQLStorageServices theStorageServices) {
Validate.notNull(theFhirContext, "theFhirContext must not be null");
Validate.notNull(theStorageServices, "theStorageServices must not be null");
switch (theFhirContext.getVersion().getVersion()) {
case DSTU3: {
IValidationSupport validationSupport = (IValidationSupport) theValidationSupport;
validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport());
org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport);
engineFactory = () -> new org.hl7.fhir.dstu3.utils.GraphQLEngine(workerContext);
break;
}
case R4: {
org.hl7.fhir.r4.hapi.ctx.IValidationSupport validationSupport = (org.hl7.fhir.r4.hapi.ctx.IValidationSupport) theValidationSupport;
validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport());
org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport);
engineFactory = () -> new org.hl7.fhir.r4.utils.GraphQLEngine(workerContext);
break;
}
case R5: {
org.hl7.fhir.r5.hapi.ctx.IValidationSupport validationSupport = (org.hl7.fhir.r5.hapi.ctx.IValidationSupport) theValidationSupport;
validationSupport = ObjectUtils.defaultIfNull(validationSupport, new org.hl7.fhir.r5.hapi.ctx.DefaultProfileValidationSupport());
org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport);
engineFactory = () -> new org.hl7.fhir.r5.utils.GraphQLEngine(workerContext);
break;
}
case DSTU2:
case DSTU2_HL7ORG:
case DSTU2_1:
default: {
throw new UnsupportedOperationException("GraphQL not supported for version: " + theFhirContext.getVersion().getVersion());
}
}
myStorageServices = theStorageServices;
}
@GraphQL
public String processGraphQlRequet(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) {
IGraphQLEngine engine = engineFactory.get();
engine.setAppInfo(theRequestDetails);
engine.setServices(myStorageServices);
try {
engine.setGraphQL(Parser.parse(theQuery));
} catch (Exception theE) {
throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString());
}
try {
if (theId != null) {
IBaseResource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart());
engine.setFocus(focus);
}
engine.execute();
StringBuilder outputBuilder = new StringBuilder();
ObjectValue output = engine.getOutput();
output.write(outputBuilder, 0, "\n");
return outputBuilder.toString();
} catch (Exception e) {
StringBuilder b = new StringBuilder();
b.append("Unable to execute GraphQL Expression: ");
int statusCode = 500;
if (e instanceof BaseServerResponseException) {
b.append("HTTP ");
statusCode = ((BaseServerResponseException) e).getStatusCode();
b.append(statusCode);
b.append(" ");
} else {
// This means it's a bug, so let's log
ourLog.error("Failure during GraphQL processing", e);
}
b.append(e.getMessage());
throw new UnclassifiedServerFailureException(statusCode, b.toString());
}
}
@Initialize
public void initialize(RestfulServer theServer) {
ourLog.trace("Initializing GraphQL provider");
if (!theServer.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context");
}
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.rest.server;
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.OptionalParam;
@ -6,6 +6,10 @@ import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
@ -18,11 +22,14 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@ -34,9 +41,8 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import ca.uhn.fhir.test.utilities.JettyUtil;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class GraphQLR4ProviderTest {
@ -216,9 +222,9 @@ public class GraphQLR4ProviderTest {
}
private static class MyStorageServices implements GraphQLEngine.IGraphQLStorageServices {
private static class MyStorageServices implements IGraphQLStorageServices {
@Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<Resource> theMatches) throws FHIRException {
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
ourLog.info("listResources of {} - {}", theType, theSearchParams);
if (theSearchParams.size() == 1) {
@ -264,8 +270,8 @@ public class GraphQLR4ProviderTest {
}
@Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReference());
public IGraphQLStorageServices.ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException {
ourLog.info("lookup from {} to {}", theContext.getIdElement().getValue(), theReference.getReferenceElement().getValue());
return null;
}

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -97,6 +98,8 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
SubscriptionTriggeringProvider subscriptionTriggeringProvider = myAppCtx.getBean(SubscriptionTriggeringProvider.class);
ourRestServer.registerProvider(subscriptionTriggeringProvider);
ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC");
ourRestServer.setServerConformanceProvider(confProvider);

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.jpa.provider.dstu3;
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.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.dstu3.model.Patient;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
public class GraphQLProviderDstu3Test extends BaseResourceProviderDstu3Test {
private Logger ourLog = LoggerFactory.getLogger(GraphQLProviderDstu3Test.class);
private IIdType myPatientId0;
@Test
public void testInstanceSimpleRead() throws IOException {
initTestPatients();
String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripReturns("{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}"), TestUtil.stripReturns(resp));
}
}
@Test
public void testSystemSimpleSearch() throws IOException {
initTestPatients();
String query = "{PatientList(given:\"given\"){name{family,given}}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripReturns("{\n" +
" \"PatientList\":[{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
" },{\n" +
" \"name\":[{\n" +
" \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" +
" }]\n" +
" }]\n" +
"}"), TestUtil.stripReturns(resp));
}
}
private void initTestPatients() {
Patient p = new Patient();
p.addName()
.setFamily("FAM")
.addGiven("GIVEN1")
.addGiven("GIVEN2");
p.addName()
.addGiven("GivenOnly1")
.addGiven("GivenOnly2");
myPatientId0 = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
p = new Patient();
p.addName()
.addGiven("GivenOnlyB1")
.addGiven("GivenOnlyB2");
ourClient.create().resource(p).execute();
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -71,7 +72,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected IGenericClient ourClient;
ResourceCountCache ourResourceCountsCache;
private TerminologyUploaderProvider myTerminologyUploaderProvider;
private Object ourGraphQLProvider;
private boolean ourRestHookSubscriptionInterceptorRequested;
@Autowired
@ -105,10 +105,10 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProvider.class);
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
myDaoRegistry = myAppCtx.getBean(DaoRegistry.class);
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider, ourGraphQLProvider);
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider);
ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC");

View File

@ -27,20 +27,17 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripReturns(resp), TestUtil.stripReturns("{\n" +
assertEquals(TestUtil.stripReturns("{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}"));
} finally {
IOUtils.closeQuietly(response);
"}"), TestUtil.stripReturns(resp));
}
}
@ -52,8 +49,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
String query = "{PatientList(given:\"given\"){name{family,given}}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
CloseableHttpResponse response = ourHttpClient.execute(httpGet);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripReturns("{\n" +
@ -70,10 +66,7 @@ public class GraphQLProviderR4Test extends BaseResourceProviderR4Test {
" }]\n" +
" }]\n" +
"}"), TestUtil.stripReturns(resp));
} finally {
IOUtils.closeQuietly(response);
}
}
private void initTestPatients() {

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -108,8 +109,7 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test {
myDaoRegistry = myAppCtx.getBean(DaoRegistry.class);
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider);
// ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
// ourRestServer.registerProvider(ourGraphQLProvider);
ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC");

View File

@ -34,7 +34,7 @@ import ca.uhn.fhirtest.config.TestR4Config;
import ca.uhn.fhirtest.config.TestR5Config;
import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -127,6 +127,7 @@ public class TestRestfulServer extends RestfulServer {
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
providers.add(myAppCtx.getBean(GraphQLProvider.class));
break;
}
case "R4": {
@ -164,7 +165,7 @@ public class TestRestfulServer extends RestfulServer {
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
// providers.add(myAppCtx.getBean(GraphQLProvider.class));
providers.add(myAppCtx.getBean(GraphQLProvider.class));
break;
}
default:

View File

@ -19,6 +19,11 @@
<artifactId>hapi-fhir-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.utilities</artifactId>
<version>${fhir_core_version}</version>
</dependency>
<!-- Server -->
<dependency>

View File

@ -1,107 +0,0 @@
package org.hl7.fhir.r4.hapi.rest.server;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.GraphQL;
import ca.uhn.fhir.rest.annotation.GraphQLQuery;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.GraphQLEngine;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GraphQLProvider {
private final IWorkerContext myWorkerContext;
private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class);
private GraphQLEngine.IGraphQLStorageServices myStorageServices;
/**
* Constructor which uses a default context and validation support object
*
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(GraphQLEngine.IGraphQLStorageServices theStorageServices) {
this(FhirContext.forR4(), new DefaultProfileValidationSupport(), theStorageServices);
}
/**
* Constructor which uses the given worker context
*
* @param theFhirContext The HAPI FHIR Context object
* @param theValidationSupport The HAPI Validation Support object
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(FhirContext theFhirContext, IValidationSupport theValidationSupport, GraphQLEngine.IGraphQLStorageServices theStorageServices) {
myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport);
myStorageServices = theStorageServices;
}
@GraphQL
public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) {
GraphQLEngine engine = new GraphQLEngine(myWorkerContext);
engine.setAppInfo(theRequestDetails);
engine.setServices(myStorageServices);
try {
engine.setGraphQL(Parser.parse(theQuery));
} catch (Exception theE) {
throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString());
}
try {
if (theId != null) {
Resource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart());
engine.setFocus(focus);
}
engine.execute();
StringBuilder outputBuilder = new StringBuilder();
ObjectValue output = engine.getOutput();
output.write(outputBuilder, 0, "\n");
return outputBuilder.toString();
} catch (Exception e) {
StringBuilder b = new StringBuilder();
b.append("Unable to execute GraphQL Expression: ");
int statusCode = 500;
if (e instanceof BaseServerResponseException) {
b.append("HTTP ");
statusCode = ((BaseServerResponseException) e).getStatusCode();
b.append(statusCode);
b.append(" ");
} else {
// This means it's a bug, so let's log
ourLog.error("Failure during GraphQL processing", e);
}
b.append(e.getMessage());
throw new UnclassifiedServerFailureException(statusCode, b.toString());
}
}
@Initialize
public void initialize(RestfulServer theServer) {
ourLog.trace("Initializing GraphQL provider");
if (theServer.getFhirContext().getVersion().getVersion() != FhirVersionEnum.R4) {
throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context");
}
}
}

View File

@ -16,7 +16,6 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -33,8 +32,8 @@ public class GraphQLEngineTest {
return obs;
}
private GraphQLEngine.IGraphQLStorageServices createStorageServices() throws FHIRException {
GraphQLEngine.IGraphQLStorageServices retVal = mock(GraphQLEngine.IGraphQLStorageServices.class);
private IGraphQLStorageServices createStorageServices() throws FHIRException {
IGraphQLStorageServices retVal = mock(IGraphQLStorageServices.class);
when(retVal.lookup(nullable(Object.class), nullable(Resource.class), nullable(Reference.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) {
@ -46,7 +45,7 @@ public class GraphQLEngineTest {
if (reference.getReference().equalsIgnoreCase("Patient/123")) {
Patient p = new Patient();
p.getBirthDateElement().setValueAsString("2011-02-22");
return new GraphQLEngine.IGraphQLStorageServices.ReferenceResolution(context, p);
return new IGraphQLStorageServices.ReferenceResolution(context, p);
}
ourLog.info("Not found!");

View File

@ -1,107 +0,0 @@
package org.hl7.fhir.r5.hapi.rest.server;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.GraphQL;
import ca.uhn.fhir.rest.annotation.GraphQLQuery;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r5.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.utils.GraphQLEngine;
import org.hl7.fhir.utilities.graphql.ObjectValue;
import org.hl7.fhir.utilities.graphql.Parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GraphQLProvider {
private final IWorkerContext myWorkerContext;
private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class);
private GraphQLEngine.IGraphQLStorageServices myStorageServices;
/**
* Constructor which uses a default context and validation support object
*
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(GraphQLEngine.IGraphQLStorageServices theStorageServices) {
this(FhirContext.forR5(), new DefaultProfileValidationSupport(), theStorageServices);
}
/**
* Constructor which uses the given worker context
*
* @param theFhirContext The HAPI FHIR Context object
* @param theValidationSupport The HAPI Validation Support object
* @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine)
*/
public GraphQLProvider(FhirContext theFhirContext, IValidationSupport theValidationSupport, GraphQLEngine.IGraphQLStorageServices theStorageServices) {
myWorkerContext = new HapiWorkerContext(theFhirContext, theValidationSupport);
myStorageServices = theStorageServices;
}
@GraphQL
public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) {
GraphQLEngine engine = new GraphQLEngine(myWorkerContext);
engine.setAppInfo(theRequestDetails);
engine.setServices(myStorageServices);
try {
engine.setGraphQL(Parser.parse(theQuery));
} catch (Exception theE) {
throw new InvalidRequestException("Unable to parse GraphQL Expression: " + theE.toString());
}
try {
if (theId != null) {
Resource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart());
engine.setFocus(focus);
}
engine.execute();
StringBuilder outputBuilder = new StringBuilder();
ObjectValue output = engine.getOutput();
output.write(outputBuilder, 0, "\n");
return outputBuilder.toString();
} catch (Exception e) {
StringBuilder b = new StringBuilder();
b.append("Unable to execute GraphQL Expression: ");
int statusCode = 500;
if (e instanceof BaseServerResponseException) {
b.append("HTTP ");
statusCode = ((BaseServerResponseException) e).getStatusCode();
b.append(statusCode);
b.append(" ");
} else {
// This means it's a bug, so let's log
ourLog.error("Failure during GraphQL processing", e);
}
b.append(e.getMessage());
throw new UnclassifiedServerFailureException(statusCode, b.toString());
}
}
@Initialize
public void initialize(RestfulServer theServer) {
ourLog.trace("Initializing GraphQL provider");
if (theServer.getFhirContext().getVersion().getVersion() != FhirVersionEnum.R5) {
throw new ConfigurationException("Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context");
}
}
}

View File

@ -88,16 +88,28 @@
</action>
<action type="add">
Support for the new R5 draft resources has been added. This support includes the client,
<![CDATA[
<b>New Feature</b>:
server, and JPA server. Note that these definitions will change as the R5 standard is
modified until it is released, so use with caution!
]]>
</action>
<action type="add">
<![CDATA[
<b>New Feature</b>:
A new interceptor called
<![CDATA[<code>ConsentInterceptor</code>]]> has been added. This interceptor allows
<code>ConsentInterceptor</code> has been added. This interceptor allows
JPA based servers to make appropriate consent decisions related to resources that
and operations that are being returned. See
<![CDATA[<a href="http://hapifhir.io/doc_rest_server_security.html">Server Security</a>]]>
<a href="http://hapifhir.io/doc_rest_server_security.html">Server Security</a>
for more information.
]]>
</action>
<action type="add">
<![CDATA[
<b>New Feature</b>:
The JPA server now supports GraphQL for DSTU3 / R4 / R5 servers.
]]>
</action>
<action type="add">
Several enhancements have been made to the <![CDATA[<code>AuthorizationInterceptor</code>]]>:
@ -105,7 +117,8 @@
<ul>
<li>The interceptor now registers against the <code>STORAGE_PRESHOW_RESOURCES</code> interceptor hook,
which allows it to successfully authorize JPA operations that don't actually return resource content,
such as GraphQL responses.</li>
such as GraphQL responses, and resources that have been filtered using the <code>_elements</code>
parameter.</li>
<li>
</li>The rule list is now cached on a per-request basis, which should improve performance</ul>
]]>