Merge with master

This commit is contained in:
Matti Uusitalo 2019-04-25 10:44:07 +03:00
commit b95b4cf110
397 changed files with 14748 additions and 13224 deletions

View File

@ -1,6 +1,9 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
--- ---

View File

@ -0,0 +1,22 @@
---
name: Feature Request or anything else
about: Anything that is not a bug report
title: ''
labels: ''
assignees: ''
---
NOTE: Before filing a ticket, please see the following URL:
https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help
**Describe the issue**
A clear and concise description of what the feature request is. Please include details about what problem you are ultimately trying to solve (i.e. what are you building with HAPI FHIR) and how this feature would help.
**Environment (please complete the following information):**
- HAPI FHIR Version
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here.

0
.travis.yml Normal file → Executable file
View File

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.demo;
import javax.persistence.EntityManagerFactory; import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource; import javax.sql.DataSource;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -78,16 +80,18 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
return FhirServerConfigCommon.loggingInterceptor(); return FhirServerConfigCommon.loggingInterceptor();
} }
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
return FhirServerConfigCommon.getResponseHighlighterInterceptor(); return FhirServerConfigCommon.getResponseHighlighterInterceptor();
} }

View File

@ -11,14 +11,12 @@ import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
@ -101,8 +99,9 @@ public class FhirServerConfigCommon {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public static IServerInterceptor loggingInterceptor() { public static LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -114,8 +113,9 @@ public class FhirServerConfigCommon {
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
public static IServerInterceptor getResponseHighlighterInterceptor() { public static ResponseHighlighterInterceptor getResponseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal; return retVal;
} }

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.demo;
import javax.persistence.EntityManagerFactory; import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource; import javax.sql.DataSource;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -82,16 +84,18 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
return FhirServerConfigCommon.loggingInterceptor(); return FhirServerConfigCommon.loggingInterceptor();
} }
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
return FhirServerConfigCommon.getResponseHighlighterInterceptor(); return FhirServerConfigCommon.getResponseHighlighterInterceptor();
} }

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
@ -61,8 +62,8 @@ public class JpaServerDemo extends RestfulServer {
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class); ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
setResourceProviders(beans); registerProviders(beans.createProviders());
/* /*
* The system provider implements non-resource-type methods, such as * The system provider implements non-resource-type methods, such as
@ -76,7 +77,7 @@ public class JpaServerDemo extends RestfulServer {
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
setPlainProviders(systemProvider); registerProviders(systemProvider);
/* /*
* The conformance provider exports the supported resources, search parameters, etc for * The conformance provider exports the supported resources, search parameters, etc for
@ -108,7 +109,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives * This server tries to dynamically generate narratives
*/ */
FhirContext ctx = getFhirContext(); FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext())); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/* /*
* Default to JSON and pretty printing * Default to JSON and pretty printing

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
@ -61,8 +62,8 @@ public class JpaServerDemoDstu2 extends RestfulServer {
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class); ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
setResourceProviders(beans); registerProviders(beans.createProviders());
/* /*
* The system provider implements non-resource-type methods, such as * The system provider implements non-resource-type methods, such as
@ -76,7 +77,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
setPlainProviders(systemProvider); registerProvider(systemProvider);
/* /*
* The conformance provider exports the supported resources, search parameters, etc for * The conformance provider exports the supported resources, search parameters, etc for
@ -108,7 +109,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
* This server tries to dynamically generate narratives * This server tries to dynamically generate narratives
*/ */
FhirContext ctx = getFhirContext(); FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext())); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/* /*
* Default to JSON and pretty printing * Default to JSON and pretty printing

View File

@ -183,8 +183,8 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<source>1.6</source> <source>1.8</source>
<target>1.6</target> <target>1.8</target>
</configuration> </configuration>
</plugin> </plugin>

View File

@ -7,7 +7,6 @@ import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -94,8 +93,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -107,9 +107,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal; return retVal;
} }

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.ETagSupportEnum;
@ -47,14 +48,14 @@ public class JpaServerDemo extends RestfulServer {
* file which is automatically generated as a part of hapi-fhir-jpaserver-base and * file which is automatically generated as a part of hapi-fhir-jpaserver-base and
* contains bean definitions for a resource provider for each resource type * contains bean definitions for a resource provider for each resource type
*/ */
List<IResourceProvider> beans = myAppCtx.getBean("myResourceProvidersDstu3", List.class); ResourceProviderFactory beans = myAppCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
setResourceProviders(beans); registerProviders(beans.createProviders());
/* /*
* The system provider implements non-resource-type methods, such as * The system provider implements non-resource-type methods, such as
* transaction, and global history. * transaction, and global history.
*/ */
setPlainProviders(myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class)); registerProviders(myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class));
/* /*
* The conformance provider exports the supported resources, search parameters, etc for * The conformance provider exports the supported resources, search parameters, etc for
@ -75,7 +76,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives * This server tries to dynamically generate narratives
*/ */
FhirContext ctx = getFhirContext(); FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext())); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/* /*
* Default to JSON and pretty printing * Default to JSON and pretty printing

View File

@ -1,5 +1,8 @@
package example; package example;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
@ -13,6 +16,7 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.*; import ca.uhn.fhir.rest.server.interceptor.auth.*;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -101,7 +105,8 @@ public class AuthorizationInterceptors {
@IdParam IdDt theId, @IdParam IdDt theId,
@ResourceParam Patient theResource, @ResourceParam Patient theResource,
@ConditionalUrlParam String theConditionalUrl, @ConditionalUrlParam String theConditionalUrl,
RequestDetails theRequestDetails) { ServletRequestDetails theRequestDetails,
IInterceptorBroadcaster theInterceptorBroadcaster) {
// If we're processing a conditional URL... // If we're processing a conditional URL...
if (isNotBlank(theConditionalUrl)) { if (isNotBlank(theConditionalUrl)) {
@ -111,20 +116,25 @@ public class AuthorizationInterceptors {
// and supply the actual ID that's being updated // and supply the actual ID that's being updated
IdDt actual = new IdDt("Patient", "1123"); IdDt actual = new IdDt("Patient", "1123");
// There are a number of possible constructors for ActionRequestDetails.
// You should supply as much detail about the sub-operation as possible
IServerInterceptor.ActionRequestDetails subRequest =
new IServerInterceptor.ActionRequestDetails(theRequestDetails, actual);
// Notify the interceptors
subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE);
} }
// In a real server, perhaps we would process the conditional // In a real server, perhaps we would process the conditional
// request differently and follow a separate path. Either way, // request differently and follow a separate path. Either way,
// let's pretend there is some storage code here. // let's pretend there is some storage code here.
theResource.setId(theId.withVersion("2")); theResource.setId(theId.withVersion("2"));
// Notify the interceptor framework when we're about to perform an update. This is
// useful as the authorization interceptor will pick this event up and use it
// to factor into a decision about whether the operation should be allowed to proceed.
IBaseResource previousContents = theResource;
IBaseResource newContents = theResource;
HookParams params = new HookParams()
.add(IBaseResource.class, previousContents)
.add(IBaseResource.class, newContents)
.add(RequestDetails.class, theRequestDetails)
.add(ServletRequestDetails.class, theRequestDetails);
theInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, params);
MethodOutcome retVal = new MethodOutcome(); MethodOutcome retVal = new MethodOutcome();
retVal.setCreated(true); retVal.setCreated(true);
retVal.setResource(theResource); retVal.setResource(theResource);

View File

@ -33,7 +33,7 @@ public interface IRestfulClient extends IBasicClient {
/** /**
* The "@Search" annotation indicates that this method supports the * The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with * search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This * this annotation, to support many different search criteria. This
* example searches by family name. * example searches by family name.
* *

View File

@ -1,16 +1,15 @@
package example; package example;
import java.io.IOException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.NarrativeStatusEnum; import ca.uhn.fhir.model.dstu2.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
@SuppressWarnings("unused")
public class Narrative { public class Narrative {
public static void main(String[] args) throws DataFormatException, IOException { public static void main(String[] args) throws DataFormatException {
//START SNIPPET: example1 //START SNIPPET: example1
Patient patient = new Patient(); Patient patient = new Patient();
@ -21,7 +20,7 @@ patient.addAddress().addLine("742 Evergreen Terrace").setCity("Springfield").set
FhirContext ctx = FhirContext.forDstu2(); FhirContext ctx = FhirContext.forDstu2();
// Use the narrative generator // Use the narrative generator
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ctx)); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
// Encode the output, including the narrative // Encode the output, including the narrative
String output = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); String output = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);

View File

@ -1,18 +1,17 @@
package example; package example;
import java.io.IOException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator;
@SuppressWarnings("unused")
public class NarrativeGenerator { public class NarrativeGenerator {
public void testGenerator() throws IOException { public void testGenerator() {
//START SNIPPET: gen //START SNIPPET: gen
FhirContext ctx = FhirContext.forDstu2(); FhirContext ctx = FhirContext.forDstu2();
String propFile = "classpath:/com/foo/customnarrative.properties"; String propFile = "classpath:/com/foo/customnarrative.properties";
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(ctx, propFile); CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(propFile);
ctx.setNarrativeGenerator(gen); ctx.setNarrativeGenerator(gen);
//END SNIPPET: gen //END SNIPPET: gen

View File

@ -0,0 +1,4 @@
package example;
public class NewInterceptors {
}

View File

@ -55,7 +55,7 @@ public class RestfulObservationResourceProvider implements IResourceProvider {
/** /**
* The "@Search" annotation indicates that this method supports the * The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with * search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This * this annotation, to support many different search criteria. This
* example searches by family name. * example searches by family name.
* *

View File

@ -56,7 +56,7 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
/** /**
* The "@Search" annotation indicates that this method supports the * The "@Search" annotation indicates that this method supports the
* search operation. You may have many different method annotated with * search operation. You may have many different methods annotated with
* this annotation, to support many different search criteria. This * this annotation, to support many different search criteria. This
* example searches by family name. * example searches by family name.
* *

View File

@ -234,6 +234,12 @@ public class ValidatorExamples {
return null; return null;
} }
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
// TODO: implement
return null;
}
@Override @Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
// TODO: implement // TODO: implement

View File

@ -1,8 +1,8 @@
package example.interceptor; package example.interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage;

View File

@ -139,6 +139,10 @@
<groupId>org.apache.derby</groupId> <groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId> <artifactId>derbyclient</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.checkerframework</groupId>
<artifactId>checker-compat-qual</artifactId>
</dependency>
</ignoredDependencies> </ignoredDependencies>
<ignoredResourcePatterns> <ignoredResourcePatterns>
<ignoredResourcePattern>changelog.txt</ignoredResourcePattern> <ignoredResourcePattern>changelog.txt</ignoredResourcePattern>
@ -198,7 +202,6 @@
<linksource>true</linksource> <linksource>true</linksource>
<verbose>false</verbose> <verbose>false</verbose>
<debug>false</debug> <debug>false</debug>
<additionalparam>-Xdoclint:none</additionalparam>
<additionalJOption>-Xdoclint:none</additionalJOption> <additionalJOption>-Xdoclint:none</additionalJOption>
</configuration> </configuration>
<executions> <executions>

View File

@ -101,13 +101,36 @@ public class HapiLocalizer {
String formatString = getFormatString(theQualifiedKey); String formatString = getFormatString(theQualifiedKey);
format = new MessageFormat(formatString.trim()); format = newMessageFormat(formatString);
myKeyToMessageFormat.put(theQualifiedKey, format); myKeyToMessageFormat.put(theQualifiedKey, format);
return format.format(theParameters); return format.format(theParameters);
} }
return getFormatString(theQualifiedKey); return getFormatString(theQualifiedKey);
} }
MessageFormat newMessageFormat(String theFormatString) {
StringBuilder pattern = new StringBuilder(theFormatString.trim());
for (int i = 0; i < (pattern.length()-1); i++) {
if (pattern.charAt(i) == '{') {
char nextChar = pattern.charAt(i+1);
if (nextChar >= '0' && nextChar <= '9') {
continue;
}
pattern.replace(i, i+1, "'{'");
int closeBraceIndex = pattern.indexOf("}", i);
if (closeBraceIndex > 0) {
i = closeBraceIndex;
pattern.replace(i, i+1, "'}'");
}
}
}
return new MessageFormat(pattern.toString());
}
protected void init() { protected void init() {
for (String nextName : myBundleNames) { for (String nextName : myBundleNames) {
myBundle.add(ResourceBundle.getBundle(nextName)); myBundle.add(ResourceBundle.getBundle(nextName));

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api; package ca.uhn.fhir.interceptor.api;
/*- /*-
* #%L * #%L
* HAPI FHIR Model * HAPI FHIR - Core Library
* %% * %%
* Copyright (C) 2014 - 2019 University Health Network * Copyright (C) 2014 - 2019 University Health Network
* %% * %%
@ -27,12 +27,8 @@ import java.lang.annotation.Target;
/** /**
* This annotation should be placed on * This annotation should be placed on
* {@link Interceptor Subscription Interceptor} * {@link Interceptor}
* bean methods. * bean methods.
* <p>
* Methods with this annotation are invoked immediately before a REST HOOK
* subscription delivery
* </p>
* *
* @see Interceptor * @see Interceptor
*/ */
@ -43,6 +39,15 @@ public @interface Hook {
/** /**
* Provides the specific point where this method should be invoked * Provides the specific point where this method should be invoked
*/ */
Pointcut[] value(); Pointcut value();
/**
* The order that interceptors should be called in. Lower numbers happen before higher numbers. Default is 0
* and allowable values can be positive or negative or 0.
* <p>
* If no order is specified, or the order is set to <code>0</code> (the default order),
* the order specified at the interceptor type level will take precedence.
* </p>
*/
int order() default Interceptor.DEFAULT_ORDER;
} }

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api; package ca.uhn.fhir.interceptor.api;
/*- /*-
* #%L * #%L
* HAPI FHIR Model * HAPI FHIR - Core Library
* %% * %%
* Copyright (C) 2014 - 2019 University Health Network * Copyright (C) 2014 - 2019 University Health Network
* %% * %%
@ -23,10 +23,14 @@ package ca.uhn.fhir.jpa.model.interceptor.api;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class HookParams { public class HookParams {
@ -48,24 +52,52 @@ public class HookParams {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> void add(T theNext) { public <T> HookParams add(@Nonnull T theNext) {
Class<T> nextClass = (Class<T>) theNext.getClass(); Class<T> nextClass = (Class<T>) theNext.getClass();
add(nextClass, theNext); add(nextClass, theNext);
return this;
} }
public <T> HookParams add(Class<T> theType, T theParam) { public <T> HookParams add(Class<T> theType, T theParam) {
return doAdd(theType, theParam);
}
// /**
// * This is useful for providing a lazy-loaded (generally expensive to create)
// * parameters
// */
// public <T> HookParams addSupplier(Class<T> theType, Supplier<T> theParam) {
// return doAdd(theType, theParam);
// }
private <T> HookParams doAdd(Class<T> theType, Object theParam) {
Validate.isTrue(theType.equals(Supplier.class) == false, "Can not add parameters of type Supplier");
myParams.put(theType, theParam); myParams.put(theType, theParam);
return this; return this;
} }
public <T> T get(Class<T> theParamType) {
return get(theParamType, 0);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T get(Class<T> theParamType, int theIndex) { public <T> T get(Class<T> theParamType, int theIndex) {
List<T> objects = (List<T>) myParams.get(theParamType); List<Object> objects = myParams.get(theParamType);
T retVal = null; Object retVal = null;
if (objects.size() > theIndex) { if (objects.size() > theIndex) {
retVal = objects.get(theIndex); retVal = objects.get(theIndex);
} }
return retVal;
retVal = unwrapValue(retVal);
return (T) retVal;
}
private Object unwrapValue(Object theValue) {
if (theValue instanceof Supplier) {
theValue = ((Supplier) theValue).get();
}
return theValue;
} }
/** /**
@ -73,10 +105,31 @@ public class HookParams {
* key is the param type and the value is the actual instance * key is the param type and the value is the actual instance
*/ */
public ListMultimap<Class<?>, Object> getParamsForType() { public ListMultimap<Class<?>, Object> getParamsForType() {
return Multimaps.unmodifiableListMultimap(myParams); ArrayListMultimap<Class<?>, Object> retVal = ArrayListMultimap.create();
myParams.entries().forEach(entry -> retVal.put(entry.getKey(), unwrapValue(entry.getValue())));
return Multimaps.unmodifiableListMultimap(retVal);
} }
public Collection<Object> values() { public Collection<Object> values() {
return Collections.unmodifiableCollection(myParams.values()); return
Collections.unmodifiableCollection(myParams.values())
.stream()
.map(t -> unwrapValue(t))
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
public <T> HookParams addIfMatchesType(Class<T> theType, Object theParam) {
if (theParam == null) {
add(theType, null);
} else {
if (theType.isAssignableFrom(theParam.getClass())) {
T param = (T) theParam;
add(theType, param);
} else {
add(theType, null);
}
}
return this;
} }
} }

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api; package ca.uhn.fhir.interceptor.api;
/*- /*-
* #%L * #%L
* HAPI FHIR Model * HAPI FHIR - Core Library
* %% * %%
* Copyright (C) 2014 - 2019 University Health Network * Copyright (C) 2014 - 2019 University Health Network
* %% * %%
@ -29,8 +29,8 @@ import com.google.common.annotations.VisibleForTesting;
*/ */
@FunctionalInterface @FunctionalInterface
@VisibleForTesting @VisibleForTesting
public interface IAnonymousLambdaHook { public interface IAnonymousInterceptor {
void invoke(HookParams theArgs); void invoke(Pointcut thePointcut, HookParams theArgs);
} }

View File

@ -0,0 +1,42 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* 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%
*/
public interface IInterceptorBroadcaster {
/**
* Invoke registered interceptor hook methods for the given Pointcut.
*
* @return Returns <code>false</code> if any of the invoked hook methods returned
* <code>false</code>, and returns <code>true</code> otherwise.
*/
boolean callHooks(Pointcut thePointcut, HookParams theParams);
/**
* Invoke registered interceptor hook methods for the given Pointcut. This method
* should only be called for pointcuts that return a type other than
* <code>void</code> or <code>boolean</code>
*
* @return Returns the object returned by the first hook method that did not return <code>null</code>
*/
Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams);
}

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.interceptor.api;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* 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 javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
public interface IInterceptorService extends IInterceptorBroadcaster {
/**
* Register an interceptor that will be used in a {@link ThreadLocal} context.
* This means that events will only be broadcast to the given interceptor if
* they were fired from the current thread.
* <p>
* Note that it is almost always desirable to call this method with a
* try-finally statement that removes the interceptor afterwards, since
* this can lead to memory leakage, poor performance due to ever-increasing
* numbers of interceptors, etc.
* </p>
* <p>
* Note that most methods such as {@link #getAllRegisteredInterceptors()} and
* {@link #unregisterAllInterceptors()} do not affect thread local interceptors
* as they are kept in a separate list.
* </p>
*
* @param theInterceptor The interceptor
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerThreadLocalInterceptor(Object theInterceptor);
/**
* Unregisters a ThreadLocal interceptor
*
* @param theInterceptor The interceptor
* @see #registerThreadLocalInterceptor(Object)
*/
void unregisterThreadLocalInterceptor(Object theInterceptor);
/**
* Register an interceptor. This method has no effect if the given interceptor is already registered.
*
* @param theInterceptor The interceptor to register
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerInterceptor(Object theInterceptor);
/**
* Unregister an interceptor. This method has no effect if the given interceptor is not already registered.
*
* @param theInterceptor The interceptor to unregister
*/
void unregisterInterceptor(Object theInterceptor);
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor);
/**
* Returns all currently registered interceptors (excluding any thread local interceptors).
*/
List<Object> getAllRegisteredInterceptors();
/**
* Unregisters all registered interceptors. Note that this method does not unregister
* any {@link #registerThreadLocalInterceptor(Object) thread local interceptors}.
*/
void unregisterAllInterceptors();
void unregisterInterceptors(@Nullable Collection<?> theInterceptors);
void registerInterceptors(@Nullable Collection<?> theInterceptors);
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.model.interceptor.api; package ca.uhn.fhir.interceptor.api;
/*- /*-
* #%L * #%L
* HAPI FHIR Model * HAPI FHIR - Core Library
* %% * %%
* Copyright (C) 2014 - 2019 University Health Network * Copyright (C) 2014 - 2019 University Health Network
* %% * %%
@ -33,9 +33,13 @@ import java.lang.annotation.Target;
public @interface Interceptor { public @interface Interceptor {
/** /**
* @return Declares that an interceptor should be manually registered with the registry, * @see #order()
* and should not auto-register using Spring autowiring.
*/ */
boolean manualRegistration() default false; int DEFAULT_ORDER = 0;
/**
* The order that interceptors should be called in. Lower numbers happen before higher numbers. Default is 0
* and allowable values can be positive or negative or 0.
*/
int order() default DEFAULT_ORDER;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,579 @@
package ca.uhn.fhir.interceptor.executor;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* 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.interceptor.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster {
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class);
private final List<Object> myInterceptors = new ArrayList<>();
private final ListMultimap<Pointcut, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
private final Object myRegistryMutex = new Object();
private final ThreadLocal<ListMultimap<Pointcut, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>();
private String myName;
private boolean myThreadlocalInvokersEnabled = true;
/**
* Constructor which uses a default name of "default"
*/
public InterceptorService() {
this("default");
}
/**
* Constructor
*
* @param theName The name for this registry (useful for troubleshooting)
*/
public InterceptorService(String theName) {
super();
myName = theName;
}
/**
* Are threadlocal interceptors enabled on this registry (defaults to true)
*/
public boolean isThreadlocalInvokersEnabled() {
return myThreadlocalInvokersEnabled;
}
/**
* Are threadlocal interceptors enabled on this registry (defaults to true)
*/
public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) {
myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled;
}
@VisibleForTesting
List<Object> getGlobalInterceptorsForUnitTest() {
return myInterceptors;
}
@Override
@VisibleForTesting
public void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor) {
registerAnonymousInterceptor(thePointcut, Interceptor.DEFAULT_ORDER, theInterceptor);
}
public void setName(String theName) {
myName = theName;
}
@Override
public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) {
Validate.notNull(thePointcut);
Validate.notNull(theInterceptor);
synchronized (myRegistryMutex) {
myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder));
if (!isInterceptorAlreadyRegistered(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
}
@Override
public List<Object> getAllRegisteredInterceptors() {
synchronized (myRegistryMutex) {
List<Object> retVal = new ArrayList<>();
retVal.addAll(myInterceptors);
return Collections.unmodifiableList(retVal);
}
}
@Override
@VisibleForTesting
public void unregisterAllInterceptors() {
synchronized (myRegistryMutex) {
myAnonymousInvokers.clear();
myGlobalInvokers.clear();
myInterceptors.clear();
}
}
@Override
public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
if (theInterceptors != null) {
theInterceptors.forEach(t -> unregisterInterceptor(t));
}
}
@Override
public void registerInterceptors(@Nullable Collection<?> theInterceptors) {
if (theInterceptors != null) {
theInterceptors.forEach(t -> registerInterceptor(t));
}
}
@Override
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
if (!myThreadlocalInvokersEnabled) {
return false;
}
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers);
return !invokers.isEmpty();
}
@Override
public void unregisterThreadLocalInterceptor(Object theInterceptor) {
if (myThreadlocalInvokersEnabled) {
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
if (invokers.isEmpty()) {
myThreadlocalInvokers.remove();
}
}
}
private ListMultimap<Pointcut, BaseInvoker> getThreadLocalInvokerMultimap() {
ListMultimap<Pointcut, BaseInvoker> invokers = myThreadlocalInvokers.get();
if (invokers == null) {
invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
myThreadlocalInvokers.set(invokers);
}
return invokers;
}
@Override
public boolean registerInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
if (isInterceptorAlreadyRegistered(theInterceptor)) {
return false;
}
List<HookInvoker> addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers);
if (addedInvokers.isEmpty()) {
ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName());
return false;
}
// Add to the global list
myInterceptors.add(theInterceptor);
sortByOrderAnnotation(myInterceptors);
return true;
}
}
private boolean isInterceptorAlreadyRegistered(Object theInterceptor) {
for (Object next : myInterceptors) {
if (next == theInterceptor) {
return true;
}
}
return false;
}
@Override
public void unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
myInterceptors.removeIf(t -> t == theInterceptor);
myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
}
}
private void sortByOrderAnnotation(List<Object> theObjects) {
IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>();
for (Object next : theObjects) {
Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class);
int order = orderAnnotation != null ? orderAnnotation.order() : 0;
interceptorToOrder.put(next, order);
}
theObjects.sort((a, b) -> {
Integer orderA = interceptorToOrder.get(a);
Integer orderB = interceptorToOrder.get(b);
return orderA - orderB;
});
}
@Override
public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams);
assert thePointcut.getReturnType() != void.class && thePointcut.getReturnType() != boolean.class;
Object retVal = doCallHooks(thePointcut, theParams, null);
return retVal;
}
@Override
public boolean callHooks(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams);
assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class;
Object retValObj = doCallHooks(thePointcut, theParams, true);
return (Boolean) retValObj;
}
private Object doCallHooks(Pointcut thePointcut, HookParams theParams, Object theRetVal) {
List<BaseInvoker> invokers = getInvokersForPointcut(thePointcut);
/*
* Call each hook in order
*/
for (BaseInvoker nextInvoker : invokers) {
Object nextOutcome = nextInvoker.invoke(theParams);
if (thePointcut.getReturnType() == boolean.class) {
Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome;
if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) {
ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker);
theRetVal = false;
break;
}
} else if (thePointcut.getReturnType() != void.class) {
if (nextOutcome != null) {
theRetVal = nextOutcome;
break;
}
}
}
return theRetVal;
}
@VisibleForTesting
List<Object> getInterceptorsWithInvokersForPointcut(Pointcut thePointcut) {
return getInvokersForPointcut(thePointcut)
.stream()
.map(BaseInvoker::getInterceptor)
.collect(Collectors.toList());
}
/**
* Returns an ordered list of invokers for the given pointcut. Note that
* a new and stable list is returned to.. do whatever you want with it.
*/
private List<BaseInvoker> getInvokersForPointcut(Pointcut thePointcut) {
List<BaseInvoker> invokers;
synchronized (myRegistryMutex) {
List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut);
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
List<BaseInvoker> threadLocalInvokers = null;
if (myThreadlocalInvokersEnabled) {
ListMultimap<Pointcut, BaseInvoker> pointcutToInvokers = myThreadlocalInvokers.get();
if (pointcutToInvokers != null) {
threadLocalInvokers = pointcutToInvokers.get(thePointcut);
}
}
invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers);
}
return invokers;
}
/**
* First argument must be the global invoker list!!
*/
@SafeVarargs
private final List<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) {
List<BaseInvoker> haveOne = null;
boolean haveMultiple = false;
for (List<BaseInvoker> nextInvokerList : theInvokersLists) {
if (nextInvokerList == null || nextInvokerList.isEmpty()) {
continue;
}
if (haveOne == null) {
haveOne = nextInvokerList;
} else {
haveMultiple = true;
}
}
if (haveOne == null) {
return Collections.emptyList();
}
List<BaseInvoker> retVal;
if (haveMultiple == false) {
// The global list doesn't need to be sorted every time since it's sorted on
// insertion each time. Doing so is a waste of cycles..
if (haveOne == theInvokersLists[0]) {
retVal = haveOne;
} else {
retVal = new ArrayList<>(haveOne);
retVal.sort(Comparator.naturalOrder());
}
} else {
retVal = Arrays
.stream(theInvokersLists)
.filter(t -> t != null)
.flatMap(t -> t.stream())
.sorted()
.collect(Collectors.toList());
}
return retVal;
}
/**
* Only call this when assertions are enabled, it's expensive
*/
boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) {
Validate.isTrue(theParams.getParamsForType().values().size() == thePointcut.getParameterTypes().size(), "Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t != null ? t.getClass().getSimpleName() : "null").sorted().collect(Collectors.toList()));
List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
for (Class<?> nextTypeClass : givenTypes.keySet()) {
String nextTypeName = nextTypeClass.getName();
for (Object nextParamValue : givenTypes.get(nextTypeClass)) {
Validate.isTrue(nextParamValue == null || nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue != null ? nextParamValue.getClass() : "null", nextTypeClass);
Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName);
}
}
return true;
}
private class AnonymousLambdaInvoker extends BaseInvoker {
private final IAnonymousInterceptor myHook;
private final Pointcut myPointcut;
public AnonymousLambdaInvoker(Pointcut thePointcut, IAnonymousInterceptor theHook, int theOrder) {
super(theHook, theOrder);
myHook = theHook;
myPointcut = thePointcut;
}
@Override
Object invoke(HookParams theParams) {
myHook.invoke(myPointcut, theParams);
return true;
}
}
private abstract static class BaseInvoker implements Comparable<BaseInvoker> {
private final int myOrder;
private final Object myInterceptor;
BaseInvoker(Object theInterceptor, int theOrder) {
myInterceptor = theInterceptor;
myOrder = theOrder;
}
public Object getInterceptor() {
return myInterceptor;
}
abstract Object invoke(HookParams theParams);
@Override
public int compareTo(BaseInvoker theInvoker) {
return myOrder - theInvoker.myOrder;
}
}
private static class HookInvoker extends BaseInvoker {
private final Method myMethod;
private final Class<?>[] myParameterTypes;
private final int[] myParameterIndexes;
private final Pointcut myPointcut;
/**
* Constructor
*/
private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
super(theInterceptor, theOrder);
myPointcut = theHook.value();
myParameterTypes = theHookMethod.getParameterTypes();
myMethod = theHookMethod;
Class<?> returnType = theHookMethod.getReturnType();
if (myPointcut.getReturnType().equals(boolean.class)) {
Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod);
} else if (myPointcut.getReturnType().equals(void.class)) {
Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod);
} else {
Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod);
}
myParameterIndexes = new int[myParameterTypes.length];
Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>();
for (int i = 0; i < myParameterTypes.length; i++) {
AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0));
myParameterIndexes[i] = counter.getAndIncrement();
}
myMethod.setAccessible(true);
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("method", myMethod)
.toString();
}
public Pointcut getPointcut() {
return myPointcut;
}
/**
* @return Returns true/false if the hook method returns a boolean, returns true otherwise
*/
@Override
Object invoke(HookParams theParams) {
Object[] args = new Object[myParameterTypes.length];
for (int i = 0; i < myParameterTypes.length; i++) {
Class<?> nextParamType = myParameterTypes[i];
int nextParamIndex = myParameterIndexes[i];
Object nextParamValue = theParams.get(nextParamType, nextParamIndex);
args[i] = nextParamValue;
}
// Invoke the method
try {
return myMethod.invoke(getInterceptor(), args);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (myPointcut.isShouldLogAndSwallowException(targetException)) {
ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException);
return null;
}
if (targetException instanceof RuntimeException) {
throw ((RuntimeException) targetException);
} else {
throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException);
}
} catch (Exception e) {
throw new InternalErrorException(e);
}
}
}
private static List<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<Pointcut, BaseInvoker> theInvokers) {
Class<?> interceptorClass = theInterceptor.getClass();
int typeOrder = determineOrder(interceptorClass);
List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
// Invoke the REGISTERED pointcut for any added hooks
addedInvokers.stream()
.filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut()))
.forEach(t -> t.invoke(new HookParams()));
// Register the interceptor and its various hooks
for (HookInvoker nextAddedHook : addedInvokers) {
Pointcut nextPointcut = nextAddedHook.getPointcut();
if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) {
continue;
}
theInvokers.put(nextPointcut, nextAddedHook);
}
// Make sure we're always sorted according to the order declared in
// @Order
for (Pointcut nextPointcut : theInvokers.keys()) {
List<BaseInvoker> nextInvokerList = theInvokers.get(nextPointcut);
nextInvokerList.sort(Comparator.naturalOrder());
}
return addedInvokers;
}
/**
* @return Returns a list of any added invokers
*/
private static List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
ArrayList<HookInvoker> retVal = new ArrayList<>();
for (Method nextMethod : theInterceptor.getClass().getMethods()) {
Optional<Hook> hook = findAnnotation(nextMethod, Hook.class);
if (hook.isPresent()) {
int methodOrder = theTypeOrder;
int methodOrderAnnotation = hook.get().order();
if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) {
methodOrder = methodOrderAnnotation;
}
retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder));
}
}
return retVal;
}
private static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> theHookClass) {
T annotation;
if (theObject instanceof Method) {
annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true);
} else {
annotation = theObject.getAnnotation(theHookClass);
}
return Optional.ofNullable(annotation);
}
private static int determineOrder(Class<?> theInterceptorClass) {
int typeOrder = Interceptor.DEFAULT_ORDER;
Optional<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
if (typeOrderAnnotation.isPresent()) {
typeOrder = typeOrderAnnotation.get().order();
}
return typeOrder;
}
private static String toErrorString(List<String> theParameterTypes) {
return theParameterTypes
.stream()
.sorted()
.collect(Collectors.joining(","));
}
}

View File

@ -29,8 +29,8 @@ import ca.uhn.fhir.rest.api.QualifiedParamList;
public interface IQueryParameterOr<T extends IQueryParameterType> extends Serializable { public interface IQueryParameterOr<T extends IQueryParameterType> extends Serializable {
public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, QualifiedParamList theParameters); void setValuesAsQueryTokens(FhirContext theContext, String theParamName, QualifiedParamList theParameters);
public List<T> getValuesAsQueryTokens(); List<T> getValuesAsQueryTokens();
} }

View File

@ -36,16 +36,16 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative
/** /**
* Constructor * Constructor
*/ */
public BaseThymeleafNarrativeGenerator(FhirContext theFhirContext) { protected BaseThymeleafNarrativeGenerator() {
super(theFhirContext); super();
} }
@Override @Override
public boolean populateResourceNarrative(IBaseResource theResource) { public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
if (!myInitialized) { if (!myInitialized) {
initialize(); initialize();
} }
super.populateResourceNarrative(theResource); super.populateResourceNarrative(theFhirContext, theResource);
return false; return false;
} }
@ -58,7 +58,7 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative
List<String> propFileName = getPropertyFile(); List<String> propFileName = getPropertyFile();
try { try {
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(getFhirContext(), propFileName); NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(propFileName);
setManifest(manifest); setManifest(manifest);
} catch (IOException e) { } catch (IOException e) {
throw new InternalErrorException(e); throw new InternalErrorException(e);

View File

@ -23,7 +23,6 @@ package ca.uhn.fhir.narrative;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import ca.uhn.fhir.context.FhirContext;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator { public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator {
@ -40,8 +39,8 @@ public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGen
* <li>classpath:/com/package/file.properties</li> * <li>classpath:/com/package/file.properties</li>
* </ul> * </ul>
*/ */
public CustomThymeleafNarrativeGenerator(FhirContext theFhirContext, String... thePropertyFile) { public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
super(theFhirContext); super();
setPropertyFile(thePropertyFile); setPropertyFile(thePropertyFile);
} }

View File

@ -20,8 +20,6 @@ package ca.uhn.fhir.narrative;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -32,8 +30,8 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
private boolean myUseHapiServerConformanceNarrative; private boolean myUseHapiServerConformanceNarrative;
public DefaultThymeleafNarrativeGenerator(FhirContext theFhirContext) { public DefaultThymeleafNarrativeGenerator() {
super(theFhirContext); super();
} }
@Override @Override

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.narrative;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
public interface INarrativeGenerator { public interface INarrativeGenerator {
@ -32,6 +33,6 @@ public interface INarrativeGenerator {
* *
* @return Returns <code>true</code> if a narrative was actually generated * @return Returns <code>true</code> if a narrative was actually generated
*/ */
boolean populateResourceNarrative(IBaseResource theResource); boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource);
} }

View File

@ -27,14 +27,13 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.fluentpath.IFluentPath; import ca.uhn.fhir.fluentpath.IFluentPath;
import ca.uhn.fhir.narrative.INarrativeGenerator; import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.INarrative; import org.hl7.fhir.instance.model.api.INarrative;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -44,12 +43,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseNarrativeGenerator implements INarrativeGenerator { public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
private INarrativeTemplateManifest myManifest; private INarrativeTemplateManifest myManifest;
private final FhirContext myFhirContext;
public BaseNarrativeGenerator(FhirContext theFhirContext) {
Validate.notNull(theFhirContext, "theFhirContext must not be null");
myFhirContext = theFhirContext;
}
public INarrativeTemplateManifest getManifest() { public INarrativeTemplateManifest getManifest() {
return myManifest; return myManifest;
@ -59,43 +52,40 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
myManifest = theManifest; myManifest = theManifest;
} }
public FhirContext getFhirContext() { @Override
return myFhirContext; public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
List<INarrativeTemplate> templateOpt = getTemplateForElement(theFhirContext, theResource);
if (templateOpt.size() > 0) {
applyTemplate(theFhirContext, templateOpt.get(0), theResource);
return true;
} }
@Override
public boolean populateResourceNarrative(IBaseResource theResource) {
Optional<INarrativeTemplate> templateOpt = getTemplateForElement(theResource);
if (templateOpt.isPresent()) {
return applyTemplate(templateOpt.get(), theResource);
} else {
return false; return false;
} }
private List<INarrativeTemplate> getTemplateForElement(FhirContext theFhirContext, IBase theElement) {
return myManifest.getTemplateByElement(theFhirContext, getStyle(), theElement);
} }
private Optional<INarrativeTemplate> getTemplateForElement(IBase theElement) { private boolean applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) {
return myManifest.getTemplateByElement(getStyle(), theElement);
}
private boolean applyTemplate(INarrativeTemplate theTemplate, IBaseResource theResource) {
if (templateDoesntApplyToResource(theTemplate, theResource)) { if (templateDoesntApplyToResource(theTemplate, theResource)) {
return false; return false;
} }
boolean retVal = false; boolean retVal = false;
String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); String resourceName = theFhirContext.getResourceDefinition(theResource).getName();
String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName); String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName);
// Narrative templates define a path within the resource that they apply to. Here, we're // Narrative templates define a path within the resource that they apply to. Here, we're
// finding anywhere in the resource that gets a narrative // finding anywhere in the resource that gets a narrative
List<IBase> targets = findElementsInResourceRequiringNarratives(theResource, contextPath); List<IBase> targets = findElementsInResourceRequiringNarratives(theFhirContext, theResource, contextPath);
for (IBase nextTargetContext : targets) { for (IBase nextTargetContext : targets) {
// Extract [element].text of type Narrative // Extract [element].text of type Narrative
INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(nextTargetContext); INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(theFhirContext, nextTargetContext);
// Create the actual narrative text // Create the actual narrative text
String narrative = applyTemplate(theTemplate, nextTargetContext); String narrative = applyTemplate(theFhirContext, theTemplate, nextTargetContext);
narrative = cleanWhitespace(narrative); narrative = cleanWhitespace(narrative);
if (isNotBlank(narrative)) { if (isNotBlank(narrative)) {
@ -112,13 +102,13 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return retVal; return retVal;
} }
private INarrative getOrCreateNarrativeChildElement(IBase nextTargetContext) { private INarrative getOrCreateNarrativeChildElement(FhirContext theFhirContext, IBase nextTargetContext) {
BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) getFhirContext().getElementDefinition(nextTargetContext.getClass()); BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(nextTargetContext.getClass());
BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text"); BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text");
List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext); List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext);
INarrative nextTargetNarrative; INarrative nextTargetNarrative;
if (existing.isEmpty()) { if (existing.isEmpty()) {
nextTargetNarrative = (INarrative) getFhirContext().getElementDefinition("narrative").newInstance(); nextTargetNarrative = (INarrative) theFhirContext.getElementDefinition("narrative").newInstance();
targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative); targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative);
} else { } else {
nextTargetNarrative = (INarrative) existing.get(0); nextTargetNarrative = (INarrative) existing.get(0);
@ -126,15 +116,15 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return nextTargetNarrative; return nextTargetNarrative;
} }
private List<IBase> findElementsInResourceRequiringNarratives(IBaseResource theResource, String theContextPath) { private List<IBase> findElementsInResourceRequiringNarratives(FhirContext theFhirContext, IBaseResource theResource, String theContextPath) {
if (myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
return Collections.singletonList(theResource); return Collections.singletonList(theResource);
} }
IFluentPath fhirPath = myFhirContext.newFluentPath(); IFluentPath fhirPath = theFhirContext.newFluentPath();
return fhirPath.evaluate(theResource, theContextPath, IBase.class); return fhirPath.evaluate(theResource, theContextPath, IBase.class);
} }
protected abstract String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext); protected abstract String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext);
private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) { private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) {
boolean retVal = false; boolean retVal = false;
@ -156,7 +146,7 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
return retVal; return retVal;
} }
protected abstract TemplateTypeEnum getStyle(); protected abstract EnumSet<TemplateTypeEnum> getStyle();
/** /**
* Trims the superfluous whitespace out of an HTML block * Trims the superfluous whitespace out of an HTML block
@ -188,7 +178,6 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) { if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) {
b.append(' '); b.append(' ');
} }
inWhitespace = false;
b.append(nextChar); b.append(nextChar);
inWhitespace = false; inWhitespace = false;
betweenTags = false; betweenTags = false;

View File

@ -20,14 +20,16 @@ package ca.uhn.fhir.narrative2;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import java.util.Optional; import java.util.EnumSet;
import java.util.List;
public interface INarrativeTemplateManifest { public interface INarrativeTemplateManifest {
Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName); List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName);
Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName); List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName);
Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElementValue); List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElementValue);
} }

View File

@ -34,41 +34,37 @@ import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class NarrativeTemplateManifest implements INarrativeTemplateManifest { public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class); private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToResourceTypeToTemplate; private final Map<String, List<NarrativeTemplate>> myStyleToResourceTypeToTemplate;
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToDatatypeToTemplate; private final Map<String, List<NarrativeTemplate>> myStyleToDatatypeToTemplate;
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToNameToTemplate; private final Map<String, List<NarrativeTemplate>> myStyleToNameToTemplate;
private final FhirContext myCtx;
private final int myTemplateCount; private final int myTemplateCount;
private NarrativeTemplateManifest(FhirContext theFhirContext, Collection<NarrativeTemplate> theTemplates) { private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) {
myCtx = theFhirContext; Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>();
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToResourceTypeToTemplate = new HashMap<>(); Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>();
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToDatatypeToTemplate = new HashMap<>(); Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>();
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToNameToTemplate = new HashMap<>();
for (NarrativeTemplate nextTemplate : theTemplates) { for (NarrativeTemplate nextTemplate : theTemplates) {
Map<String, NarrativeTemplate> resourceTypeToTemplate = styleToResourceTypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>()); nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate);
Map<String, NarrativeTemplate> datatypeToTemplate = styleToDatatypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
Map<String, NarrativeTemplate> nameToTemplate = styleToNameToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate);
for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) { for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) {
resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate); resourceTypeToTemplate.computeIfAbsent(nextResourceType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
} }
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) { for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate); datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
} }
} }
myTemplateCount = theTemplates.size(); myTemplateCount = theTemplates.size();
myStyleToNameToTemplate = makeImmutable(styleToNameToTemplate); myStyleToNameToTemplate = makeImmutable(nameToTemplate);
myStyleToResourceTypeToTemplate = makeImmutable(styleToResourceTypeToTemplate); myStyleToResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
myStyleToDatatypeToTemplate = makeImmutable(styleToDatatypeToTemplate); myStyleToDatatypeToTemplate = makeImmutable(datatypeToTemplate);
} }
public int getNamedTemplateCount() { public int getNamedTemplateCount() {
@ -76,31 +72,31 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
} }
@Override @Override
public Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName) { public List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) {
return getFromMap(theStyle, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate); return getFromMap(theStyles, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
} }
@Override @Override
public Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName) { public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) {
return getFromMap(theStyle, theName, myStyleToNameToTemplate); return getFromMap(theStyles, theName, myStyleToNameToTemplate);
} }
@Override @Override
public Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElement) { public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) {
if (theElement instanceof IBaseResource) { if (theElement instanceof IBaseResource) {
String resourceName = myCtx.getResourceDefinition((IBaseResource) theElement).getName(); String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
return getTemplateByResourceName(theStyle, resourceName); return getTemplateByResourceName(theFhirContext, theStyles, resourceName);
} else { } else {
String datatypeName = myCtx.getElementDefinition(theElement.getClass()).getName(); String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
return getFromMap(theStyle, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate); return getFromMap(theStyles, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
} }
} }
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, String... thePropertyFilePaths) throws IOException { public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
return forManifestFileLocation(theFhirContext, Arrays.asList(thePropertyFilePaths)); return forManifestFileLocation(Arrays.asList(thePropertyFilePaths));
} }
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, Collection<String> thePropertyFilePaths) throws IOException { public static NarrativeTemplateManifest forManifestFileLocation(Collection<String> thePropertyFilePaths) throws IOException {
ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths); ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size()); List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size());
@ -109,19 +105,19 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
manifestFileContents.add(resource); manifestFileContents.add(resource);
} }
return forManifestFileContents(theFhirContext, manifestFileContents); return forManifestFileContents(manifestFileContents);
} }
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, String... theResources) throws IOException { public static NarrativeTemplateManifest forManifestFileContents(String... theResources) throws IOException {
return forManifestFileContents(theFhirContext, Arrays.asList(theResources)); return forManifestFileContents(Arrays.asList(theResources));
} }
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, Collection<String> theResources) throws IOException { public static NarrativeTemplateManifest forManifestFileContents(Collection<String> theResources) throws IOException {
List<NarrativeTemplate> templates = new ArrayList<>(); List<NarrativeTemplate> templates = new ArrayList<>();
for (String next : theResources) { for (String next : theResources) {
templates.addAll(loadProperties(next)); templates.addAll(loadProperties(next));
} }
return new NarrativeTemplateManifest(theFhirContext, templates); return new NarrativeTemplateManifest(templates);
} }
private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException { private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException {
@ -222,17 +218,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
} }
} }
private static <T> Optional<INarrativeTemplate> getFromMap(TemplateTypeEnum theStyle, T theResourceName, Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theMap) { private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
NarrativeTemplate retVal = null; return theMap
Map<T, NarrativeTemplate> resourceTypeToTemplate = theMap.get(theStyle); .getOrDefault(theKey, Collections.emptyList())
if (resourceTypeToTemplate != null) { .stream()
retVal = resourceTypeToTemplate.get(theResourceName); .filter(t->theStyles.contains(t.getTemplateType()))
} .collect(Collectors.toList());
return Optional.ofNullable(retVal);
} }
private static <T> Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> makeImmutable(Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theStyleToResourceTypeToTemplate) { private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableMap(value)); theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value));
return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate); return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
} }

View File

@ -20,12 +20,13 @@ package ca.uhn.fhir.narrative2;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.INarrativeGenerator; import ca.uhn.fhir.narrative.INarrativeGenerator;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
public class NullNarrativeGenerator implements INarrativeGenerator { public class NullNarrativeGenerator implements INarrativeGenerator {
@Override @Override
public boolean populateResourceNarrative(IBaseResource theResource) { public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
return false; return false;
} }
} }

View File

@ -45,9 +45,7 @@ import org.thymeleaf.templateresolver.DefaultTemplateResolver;
import org.thymeleaf.templateresource.ITemplateResource; import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.templateresource.StringTemplateResource; import org.thymeleaf.templateresource.StringTemplateResource;
import java.util.Map; import java.util.*;
import java.util.Optional;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -58,13 +56,13 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
/** /**
* Constructor * Constructor
*/ */
public ThymeleafNarrativeGenerator(FhirContext theFhirContext) { public ThymeleafNarrativeGenerator() {
super(theFhirContext); super();
} }
private TemplateEngine getTemplateEngine() { private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
TemplateEngine engine = new TemplateEngine(); TemplateEngine engine = new TemplateEngine();
ProfileResourceResolver resolver = new ProfileResourceResolver(); ProfileResourceResolver resolver = new ProfileResourceResolver(theFhirContext);
engine.setTemplateResolver(resolver); engine.setTemplateResolver(resolver);
if (myMessageResolver != null) { if (myMessageResolver != null) {
engine.setMessageResolver(myMessageResolver); engine.setMessageResolver(myMessageResolver);
@ -73,8 +71,8 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
@Override @Override
public Set<IProcessor> getProcessors(String theDialectPrefix) { public Set<IProcessor> getProcessors(String theDialectPrefix) {
Set<IProcessor> retVal = super.getProcessors(theDialectPrefix); Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
retVal.add(new NarrativeTagProcessor(theDialectPrefix)); retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
retVal.add(new NarrativeAttributeProcessor(theDialectPrefix)); retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
return retVal; return retVal;
} }
@ -85,25 +83,23 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
} }
@Override @Override
protected String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext) { protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
Context context = new Context(); Context context = new Context();
context.setVariable("resource", theTargetContext); context.setVariable("resource", theTargetContext);
context.setVariable("context", theTargetContext); context.setVariable("context", theTargetContext);
context.setVariable("fhirVersion", getFhirContext().getVersion().getVersion().name()); context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name());
String result = getTemplateEngine().process(theTemplate.getTemplateName(), context); return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
return result;
} }
@Override @Override
protected TemplateTypeEnum getStyle() { protected EnumSet<TemplateTypeEnum> getStyle() {
return TemplateTypeEnum.THYMELEAF; return EnumSet.of(TemplateTypeEnum.THYMELEAF);
} }
private String applyTemplateWithinTag(ITemplateContext theTemplateContext, String theName, String theElement) { private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
IEngineConfiguration configuration = theTemplateContext.getConfiguration(); IEngineConfiguration configuration = theTemplateContext.getConfiguration();
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration); IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement); final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
@ -113,20 +109,20 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
return ""; return "";
} }
Optional<INarrativeTemplate> templateOpt; List<INarrativeTemplate> templateOpt;
if (isNotBlank(theName)) { if (isNotBlank(theName)) {
templateOpt = getManifest().getTemplateByName(getStyle(), theName); templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
if (!templateOpt.isPresent()) { if (templateOpt.isEmpty()) {
throw new InternalErrorException("Unknown template name: " + theName); throw new InternalErrorException("Unknown template name: " + theName);
} }
} else { } else {
templateOpt = getManifest().getTemplateByElement(getStyle(), elementValue); templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
if (!templateOpt.isPresent()) { if (templateOpt.isEmpty()) {
throw new InternalErrorException("No template for type: " + elementValue.getClass()); throw new InternalErrorException("No template for type: " + elementValue.getClass());
} }
} }
return applyTemplate(templateOpt.get(), elementValue); return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
} }
public void setMessageResolver(IMessageResolver theMessageResolver) { public void setMessageResolver(IMessageResolver theMessageResolver) {
@ -135,9 +131,15 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
private class ProfileResourceResolver extends DefaultTemplateResolver { private class ProfileResourceResolver extends DefaultTemplateResolver {
private final FhirContext myFhirContext;
private ProfileResourceResolver(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override @Override
protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) { protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return getManifest().getTemplateByName(getStyle(), theTemplate).isPresent(); return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
} }
@Override @Override
@ -148,7 +150,9 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
@Override @Override
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) { protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return getManifest() return getManifest()
.getTemplateByName(getStyle(), theTemplate) .getTemplateByName(myFhirContext, getStyle(), theTemplate)
.stream()
.findFirst()
.map(t -> new StringTemplateResource(t.getTemplateText())) .map(t -> new StringTemplateResource(t.getTemplateText()))
.orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
} }
@ -161,8 +165,11 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
private class NarrativeTagProcessor extends AbstractElementTagProcessor { private class NarrativeTagProcessor extends AbstractElementTagProcessor {
public NarrativeTagProcessor(String dialectPrefix) { private final FhirContext myFhirContext;
NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0); super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
myFhirContext = theFhirContext;
} }
@Override @Override
@ -170,7 +177,7 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
String name = theTag.getAttributeValue("th:name"); String name = theTag.getAttributeValue("th:name");
String element = theTag.getAttributeValue("th:element"); String element = theTag.getAttributeValue("th:element");
String appliedTemplate = applyTemplateWithinTag(theTemplateContext, name, element); String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
theStructureHandler.replaceWith(appliedTemplate, false); theStructureHandler.replaceWith(appliedTemplate, false);
} }
} }
@ -181,13 +188,16 @@ public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
*/ */
private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor { private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
protected NarrativeAttributeProcessor(String theDialectPrefix) { private final FhirContext myFhirContext;
NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true); super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
myFhirContext = theFhirContext;
} }
@Override @Override
protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) { protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
String text = applyTemplateWithinTag(theContext, null, theAttributeValue); String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
theStructureHandler.setBody(text, false); theStructureHandler.setBody(text, false);
} }

View File

@ -360,7 +360,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
narr = null; narr = null;
} }
if (narr != null && narr.isEmpty()) { if (narr != null && narr.isEmpty()) {
gen.populateResourceNarrative(theResource); gen.populateResourceNarrative(myContext, theResource);
if (!narr.isEmpty()) { if (!narr.isEmpty()) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild; RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
String childName = nextChild.getChildNameByDatatype(child.getDatatype()); String childName = nextChild.getChildNameByDatatype(child.getDatatype());

View File

@ -371,7 +371,7 @@ public class XmlParser extends BaseParser /* implements IParser */ {
} }
// FIXME potential null access on narr see line 623 // FIXME potential null access on narr see line 623
if (gen != null && narr.isEmpty()) { if (gen != null && narr.isEmpty()) {
gen.populateResourceNarrative(theResource); gen.populateResourceNarrative(myContext, theResource);
} }
if (narr != null && narr.isEmpty() == false) { if (narr != null && narr.isEmpty() == false) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild; RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;

View File

@ -19,7 +19,11 @@ package ca.uhn.fhir.rest.annotation;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** /**
* Denotes a parameter for a REST method which will contain the resource actually * Denotes a parameter for a REST method which will contain the resource actually
@ -41,6 +45,9 @@ import java.lang.annotation.*;
* have multiple parameters with this annotation, so you can have one parameter * have multiple parameters with this annotation, so you can have one parameter
* which accepts the parsed resource, and another which accepts the raw request. * which accepts the parsed resource, and another which accepts the raw request.
* </p> * </p>
* <p>
* Also note that this parameter may be null if a client does not supply a body.
* </p>
*/ */
@Target(value = ElementType.PARAMETER) @Target(value = ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

View File

@ -20,6 +20,9 @@ package ca.uhn.fhir.rest.client.api;
* #L% * #L%
*/ */
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import java.io.IOException; import java.io.IOException;
/** /**
@ -35,11 +38,13 @@ public interface IClientInterceptor {
/** /**
* Fired by the client just before invoking the HTTP client request * Fired by the client just before invoking the HTTP client request
*/ */
@Hook(Pointcut.CLIENT_REQUEST)
void interceptRequest(IHttpRequest theRequest); void interceptRequest(IHttpRequest theRequest);
/** /**
* Fired by the client upon receiving an HTTP response, prior to processing that response * Fired by the client upon receiving an HTTP response, prior to processing that response
*/ */
@Hook(Pointcut.CLIENT_RESPONSE)
void interceptResponse(IHttpResponse theResponse) throws IOException; void interceptResponse(IHttpResponse theResponse) throws IOException;
} }

View File

@ -1,12 +1,13 @@
package ca.uhn.fhir.rest.client.api; package ca.uhn.fhir.rest.client.api;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List; import javax.annotation.Nonnull;
/* /*
* #%L * #%L
@ -30,6 +31,20 @@ import java.util.List;
public interface IRestfulClient { public interface IRestfulClient {
/**
* Sets the interfceptor service used by this client
*
* @since 3.8.0
*/
IInterceptorService getInterceptorService();
/**
* Sets the interfceptor service used by this client
*
* @since 3.8.0
*/
void setInterceptorService(@Nonnull IInterceptorService theInterceptorService);
/** /**
* Retrieve the contents at the given URL and parse them as a resource. This * Retrieve the contents at the given URL and parse them as a resource. This
* method could be used as a low level implementation of a read/vread/search * method could be used as a low level implementation of a read/vread/search
@ -69,11 +84,6 @@ public interface IRestfulClient {
*/ */
IHttpClient getHttpClient(); IHttpClient getHttpClient();
/**
* Returns the client interceptors that have been registered with this client
*/
List<IClientInterceptor> getInterceptors();
/** /**
* Base URL for the server, with no trailing "/" * Base URL for the server, with no trailing "/"
*/ */
@ -82,7 +92,10 @@ public interface IRestfulClient {
/** /**
* Register a new interceptor for this client. An interceptor can be used to add additional * Register a new interceptor for this client. An interceptor can be used to add additional
* logging, or add security headers, or pre-process responses, etc. * logging, or add security headers, or pre-process responses, etc.
*
* @deprecated Use {@link #getInterceptorService()} to access the list of inteerceptors, register them, and unregister them
*/ */
@Deprecated
void registerInterceptor(IClientInterceptor theInterceptor); void registerInterceptor(IClientInterceptor theInterceptor);
/** /**
@ -102,7 +115,10 @@ public interface IRestfulClient {
/** /**
* Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)} * Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)}
*
* @deprecated Use {@link #getInterceptorService()} to access the list of inteerceptors, register them, and unregister them
*/ */
@Deprecated
void unregisterInterceptor(IClientInterceptor theInterceptor); void unregisterInterceptor(IClientInterceptor theInterceptor);
/** /**

View File

@ -23,23 +23,20 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.api.annotation.SimpleSetter;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt; import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.*; import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -119,9 +116,7 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
b.append(ParameterUtil.escapeWithDefault(getPrefix().getValue())); b.append(ParameterUtil.escapeWithDefault(getPrefix().getValue()));
} }
if (myValue != null) {
b.append(ParameterUtil.escapeWithDefault(myValue.getValueAsString())); b.append(ParameterUtil.escapeWithDefault(myValue.getValueAsString()));
}
return b.toString(); return b.toString();
} }
@ -132,39 +127,16 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
} }
public TemporalPrecisionEnum getPrecision() { public TemporalPrecisionEnum getPrecision() {
if (myValue != null) {
return myValue.getPrecision(); return myValue.getPrecision();
} }
return null;
}
public Date getValue() { public Date getValue() {
if (myValue != null) {
return myValue.getValue(); return myValue.getValue();
} }
return null;
}
public DateTimeDt getValueAsDateTimeDt() {
if (myValue == null) {
return null;
}
return new DateTimeDt(myValue.getValue());
}
public InstantDt getValueAsInstantDt() {
if (myValue == null) {
return null;
}
return new InstantDt(myValue.getValue());
}
public String getValueAsString() { public String getValueAsString() {
if (myValue != null) {
return myValue.getValueAsString(); return myValue.getValueAsString();
} }
return null;
}
@Override @Override
public List<DateParam> getValuesAsQueryTokens() { public List<DateParam> getValuesAsQueryTokens() {
@ -260,6 +232,8 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
/** /**
* Constructor * Constructor
*/ */
// LEAVE THIS AS PUBLIC!!
@SuppressWarnings("WeakerAccess")
public DateParamDateTimeHolder() { public DateParamDateTimeHolder() {
super(); super();
} }

View File

@ -34,7 +34,7 @@ public class HasParam extends BaseParam implements IQueryParameterType {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String myOwningFieldName; private String myReferenceFieldName;
private String myParameterName; private String myParameterName;
private String myParameterValue; private String myParameterValue;
private String myTargetResourceType; private String myTargetResourceType;
@ -44,10 +44,10 @@ public class HasParam extends BaseParam implements IQueryParameterType {
} }
public HasParam(String theTargetResourceType, String theOwningFieldName, String theParameterName, String theParameterValue) { public HasParam(String theTargetResourceType, String theReferenceFieldName, String theParameterName, String theParameterValue) {
this(); this();
myTargetResourceType = theTargetResourceType; myTargetResourceType = theTargetResourceType;
myOwningFieldName = theOwningFieldName; myReferenceFieldName = theReferenceFieldName;
myParameterName = theParameterName; myParameterName = theParameterName;
myParameterValue = theParameterValue; myParameterValue = theParameterValue;
} }
@ -75,13 +75,13 @@ public class HasParam extends BaseParam implements IQueryParameterType {
validateColon(qualifier, colonIndex1); validateColon(qualifier, colonIndex1);
myTargetResourceType = qualifier.substring(1, colonIndex0); myTargetResourceType = qualifier.substring(1, colonIndex0);
myOwningFieldName = qualifier.substring(colonIndex0 + 1, colonIndex1); myReferenceFieldName = qualifier.substring(colonIndex0 + 1, colonIndex1);
myParameterName = qualifier.substring(colonIndex1 + 1); myParameterName = qualifier.substring(colonIndex1 + 1);
myParameterValue = theValue; myParameterValue = theValue;
} }
public String getOwningFieldName() { public String getReferenceFieldName() {
return myOwningFieldName; return myReferenceFieldName;
} }
public String getParameterName() { public String getParameterName() {

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.param; package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.CoverageIgnore;
/* /*
@ -30,11 +31,14 @@ public class StringAndListParam extends BaseAndListParam<StringOrListParam> {
return new StringOrListParam(); return new StringOrListParam();
} }
@CoverageIgnore
@Override @Override
public StringAndListParam addAnd(StringOrListParam theValue) { public StringAndListParam addAnd(StringOrListParam theValue) {
addValue(theValue); addValue(theValue);
return this; return this;
} }
public StringAndListParam addAnd(StringParam theValue) {
addValue(new StringOrListParam().addOr(theValue));
return this;
}
} }

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.rest.param; package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.util.CoverageIgnore; import org.apache.commons.lang3.Validate;
/* /*
* #%L * #%L
@ -30,11 +30,23 @@ public class TokenAndListParam extends BaseAndListParam<TokenOrListParam> {
return new TokenOrListParam(); return new TokenOrListParam();
} }
@CoverageIgnore
@Override @Override
public TokenAndListParam addAnd(TokenOrListParam theValue) { public TokenAndListParam addAnd(TokenOrListParam theValue) {
addValue(theValue); addValue(theValue);
return this; return this;
} }
/**
* @param theValue The OR values
* @return Returns a reference to this for convenient chaining
*/
public TokenAndListParam addAnd(TokenParam... theValue) {
Validate.notNull(theValue, "theValue must not be null");
TokenOrListParam orListParam = new TokenOrListParam();
for (TokenParam next : theValue) {
orListParam.add(next);
}
addValue(orListParam);
return this;
}
} }

View File

@ -77,6 +77,14 @@ public class TokenOrListParam extends BaseOrListParam<TokenOrListParam, TokenPar
return this; return this;
} }
/**
* Add a new token to this list
*/
public TokenOrListParam add(String theValue) {
add(new TokenParam(null, theValue));
return this;
}
public List<BaseCodingDt> getListAsCodings() { public List<BaseCodingDt> getListAsCodings() {
ArrayList<BaseCodingDt> retVal = new ArrayList<BaseCodingDt>(); ArrayList<BaseCodingDt> retVal = new ArrayList<BaseCodingDt>();
for (TokenParam next : getValuesAsQueryTokens()) { for (TokenParam next : getValuesAsQueryTokens()) {

View File

@ -0,0 +1,55 @@
package ca.uhn.fhir.util;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* 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 org.slf4j.Logger;
import org.slf4j.event.Level;
/**
* Utility to fill a glaring gap in SLF4j's API - The fact that you can't
* specify a log level at runtime.
*
* See here for a discussion:
* https://jira.qos.ch/browse/SLF4J-124
*/
public class LogUtil {
public static void log(Logger theLogger, Level theLevel, String theMessage, Object... theArgs) {
switch (theLevel) {
case TRACE:
theLogger.trace(theMessage, theArgs);
break;
case DEBUG:
theLogger.debug(theMessage, theArgs);
break;
case INFO:
theLogger.info(theMessage, theArgs);
break;
case WARN:
theLogger.warn(theMessage, theArgs);
break;
case ERROR:
theLogger.error(theMessage, theArgs);
break;
}
}
}

View File

@ -20,29 +20,204 @@ package ca.uhn.fhir.util;
* #L% * #L%
*/ */
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** /**
* Provides server ports * Provides server ports that are free, in order for tests to use them
*
* <p><b>
* This class is ONLY designed for unit-testing usage, as it holds on to server ports
* for a long time (potentially lots of them!) and will leave your system low on
* ports if you put it into production.
* </b></p>
* <p>
* How it works:
* <p>
* We have lots of tests that need a free port because they want to open up
* a server, and need the port to be unique and unused so that the tests can
* run multithreaded. This turns out to just be an awful problem to solve for
* lots of reasons:
* <p>
* 1. You can request a free port from the OS by calling <code>new ServerSocket(0);</code>
* and this seems to work 99% of the time, but occasionally on a heavily loaded
* server if two processes ask at the exact same time they will receive the
* same port assignment, and one will fail.
* 2. Tests run in separate processes, so we can't just rely on keeping a collection
* of assigned ports or anything like that.
* <p>
* So we solve this like this:
* <p>
* At random, this class will pick a "control port" and bind it. A control port
* is just a randomly chosen port that is a multiple of 100. If we can bind
* successfully to that port, we now own the range of "n+1 to n+99". If we can't
* bind that port, it means some other process has probably taken it so
* we'll just try again until we find an available control port.
* <p>
* Assuming we successfully bind a control port, we'll give out any available
* ports in the range "n+1 to n+99" until we've exhausted the whole set, and
* then we'll pick another control port (if we actually get asked for over
* 100 ports.. this should be a rare event).
* <p>
* This mechanism has the benefit of (fingers crossed) being bulletproof
* in terms of its ability to give out ports that are actually free, thereby
* preventing random test failures.
* <p>
* This mechanism has the drawback of never giving up a control port once
* it has assigned one. To be clear, this class is deliberately leaking
* resources. Again, no production use!
*/ */
@CoverageIgnore
public class PortUtil { public class PortUtil {
private static final int SPACE_SIZE = 100;
private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class); private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
private static final PortUtil INSTANCE = new PortUtil();
/* private static int ourPortDelay = 500;
* Non instantiable private List<ServerSocket> myControlSockets = new ArrayList<>();
private Integer myCurrentControlSocketPort = null;
private int myCurrentOffset = 0;
/**
* Constructor -
*/ */
private PortUtil() { PortUtil() {
// nothing // nothing
} }
/**
* Clear and release all control sockets
*/
synchronized void clearInstance() {
for (ServerSocket next : myControlSockets) {
ourLog.info("Releasing control port: {}", next.getLocalPort());
try {
next.close();
} catch (IOException theE) {
// ignore
}
}
myControlSockets.clear();
myCurrentControlSocketPort = null;
}
/**
* Clear and release all control sockets
*/
synchronized int getNextFreePort() {
while (true) {
// Acquire a control port
while (myCurrentControlSocketPort == null) {
int nextCandidate = (int) (Math.random() * 65000.0);
nextCandidate = nextCandidate - (nextCandidate % SPACE_SIZE);
if (nextCandidate < 10000) {
continue;
}
try {
ServerSocket server = new ServerSocket();
server.setReuseAddress(true);
server.bind(new InetSocketAddress("localhost", nextCandidate));
myControlSockets.add(server);
ourLog.info("Acquired control socket on port {}", nextCandidate);
myCurrentControlSocketPort = nextCandidate;
myCurrentOffset = 0;
} catch (IOException theE) {
ourLog.info("Candidate control socket {} is already taken", nextCandidate);
continue;
}
}
// Find a free port within the allowable range
while (true) {
myCurrentOffset++;
if (myCurrentOffset == SPACE_SIZE) {
// Current space is exhausted
myCurrentControlSocketPort = null;
break;
}
int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset;
// Try to open a port on this socket and use it
if (!isAvailable(nextCandidatePort)) {
continue;
}
// Log who asked for the port, just in case that's useful
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previousElement = Arrays.stream(stackTraceElements)
.filter(t -> !t.toString().contains("PortUtil.") && !t.toString().contains("getStackTrace"))
.findFirst()
.orElse(stackTraceElements[2]);
ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString());
try {
Thread.sleep(ourPortDelay);
} catch (InterruptedException theE) {
// ignore
}
return nextCandidatePort;
}
}
}
@VisibleForTesting
public static void setPortDelay(Integer thePortDelay) {
if (thePortDelay == null) {
thePortDelay = 500;
} else {
ourPortDelay = thePortDelay;
}
}
/**
* This method checks if we are able to bind a given port to both
* 0.0.0.0 and localhost in order to be sure it's truly available.
*/
private static boolean isAvailable(int thePort) {
ourLog.info("Testing a bind on thePort {}", thePort);
try (ServerSocket ss = new ServerSocket()) {
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress("0.0.0.0", thePort));
try (DatagramSocket ds = new DatagramSocket()) {
ds.setReuseAddress(true);
ds.connect(new InetSocketAddress("127.0.0.1", thePort));
ourLog.info("Successfully bound thePort {}", thePort);
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
try (ServerSocket ss = new ServerSocket()) {
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress("localhost", thePort));
} catch (IOException e) {
ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
return false;
}
return true;
}
/** /**
* The entire purpose here is to find an available port that can then be * The entire purpose here is to find an available port that can then be
* bound for by server in a unit test without conflicting with other tests. * bound for by server in a unit test without conflicting with other tests.
@ -51,64 +226,7 @@ public class PortUtil {
* so it can be reused across modules. Use with caution. * so it can be reused across modules. Use with caution.
*/ */
public static int findFreePort() { public static int findFreePort() {
ServerSocket server; return INSTANCE.getNextFreePort();
try {
server = new ServerSocket(0);
server.setReuseAddress(true);
int port = server.getLocalPort();
/*
* Try to connect to the newly allocated port to make sure
* it's free
*/
for (int i = 0; i < 10; i++) {
try {
Socket client = new Socket();
client.connect(new InetSocketAddress(port), 1000);
break;
} catch (Exception e) {
if (i == 9) {
throw new InternalErrorException("Can not connect to port: " + port);
}
Thread.sleep(250);
}
}
server.close();
/*
* This is an attempt to make sure the port is actually
* free before releasing it. For whatever reason on Linux
* it seems like even after we close the ServerSocket there
* is a short while where it is not possible to bind the
* port, even though it should be released by then.
*
* I don't have any solid evidence that this is a good
* way to do this, but it seems to help...
*/
for (int i = 0; i < 10; i++) {
try {
Socket client = new Socket();
client.connect(new InetSocketAddress(port), 1000);
ourLog.info("Socket still seems open");
Thread.sleep(250);
} catch (Exception e) {
break;
}
}
// ....annnd sleep a bit for the same reason.
Thread.sleep(500);
// Log who asked for the port, just in case that's useful
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previousElement = stackTraceElements[2];
ourLog.info("Returned available port {} for: {}", port, previousElement.toString());
return port;
} catch (IOException | InterruptedException e) {
throw new Error(e);
}
} }
} }

View File

@ -7,7 +7,6 @@ import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -45,20 +44,16 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/ */
public class StopWatch { public class StopWatch {
private static final NumberFormat DAY_FORMAT = new DecimalFormat("0.0");
private static final NumberFormat TEN_DAY_FORMAT = new DecimalFormat("0");
private static Long ourNowForUnitTest; private static Long ourNowForUnitTest;
private long myStarted = now(); private long myStarted = now();
private TaskTiming myCurrentTask; private TaskTiming myCurrentTask;
private LinkedList<TaskTiming> myTasks; private LinkedList<TaskTiming> myTasks;
/** /**
* Constructor * Constructor
*/ */
public StopWatch() { public StopWatch() {
super(); super();
} }
/** /**
* Constructor * Constructor
* *
@ -93,6 +88,19 @@ public class StopWatch {
} }
} }
/**
* Returns a nice human-readable display of the time taken per
* operation. Note that this may not actually output the number
* of milliseconds if the time taken per operation was very long (over
* 10 seconds)
*
* @see #formatMillis(long)
*/
public String formatMillisPerOperation(int theNumOperations) {
double millisPerOperation = (((double) getMillis()) / Math.max(1.0, theNumOperations));
return formatMillis(millisPerOperation);
}
/** /**
* Returns a string providing the durations of all tasks collected by {@link #startTask(String)} * Returns a string providing the durations of all tasks collected by {@link #startTask(String)}
*/ */
@ -261,77 +269,6 @@ public class StopWatch {
return formatMillis(getMillis()); return formatMillis(getMillis());
} }
/**
* Append a right-aligned and zero-padded numeric value to a `StringBuilder`.
*/
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
tgt.append(pfx);
if (dgt > 1) {
int pad = (dgt - 1);
for (long xa = val; xa > 9 && pad > 0; xa /= 10) {
pad--;
}
for (int xa = 0; xa < pad; xa++) {
tgt.append('0');
}
}
tgt.append(val);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(long val) {
StringBuilder buf = new StringBuilder(20);
if (val < (10 * DateUtils.MILLIS_PER_SECOND)) {
buf.append(val);
buf.append("ms");
} else if (val >= DateUtils.MILLIS_PER_DAY) {
double days = (double) val / DateUtils.MILLIS_PER_DAY;
if (days >= 10) {
buf.append(TEN_DAY_FORMAT.format(days));
buf.append(" days");
} else if (days != 1.0f) {
buf.append(DAY_FORMAT.format(days));
buf.append(" days");
} else {
buf.append(DAY_FORMAT.format(days));
buf.append(" day");
}
} else {
append(buf, "", 2, ((val % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR));
append(buf, ":", 2, ((val % DateUtils.MILLIS_PER_HOUR) / DateUtils.MILLIS_PER_MINUTE));
append(buf, ":", 2, ((val % DateUtils.MILLIS_PER_MINUTE) / DateUtils.MILLIS_PER_SECOND));
if (val <= DateUtils.MILLIS_PER_MINUTE) {
append(buf, ".", 3, (val % DateUtils.MILLIS_PER_SECOND));
}
}
return buf.toString();
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTestForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
private static class TaskTiming { private static class TaskTiming {
private long myStart; private long myStart;
private long myEnd; private long myEnd;
@ -372,4 +309,109 @@ public class StopWatch {
} }
} }
private static NumberFormat getDayFormat() {
return new DecimalFormat("0.0");
}
private static NumberFormat getTenDayFormat() {
return new DecimalFormat("0");
}
private static NumberFormat getSubMillisecondMillisFormat() {
return new DecimalFormat("0.000");
}
/**
* Append a right-aligned and zero-padded numeric value to a `StringBuilder`.
*/
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
tgt.append(pfx);
if (dgt > 1) {
int pad = (dgt - 1);
for (long xa = val; xa > 9 && pad > 0; xa /= 10) {
pad--;
}
for (int xa = 0; xa < pad; xa++) {
tgt.append('0');
}
}
tgt.append(val);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(long theMillis) {
return formatMillis((double) theMillis);
}
/**
* Formats a number of milliseconds for display (e.g.
* in a log file), tailoring the output to how big
* the value actually is.
* <p>
* Example outputs:
* </p>
* <ul>
* <li>133ms</li>
* <li>00:00:10.223</li>
* <li>1.7 days</li>
* <li>64 days</li>
* </ul>
*/
public static String formatMillis(double theMillis) {
StringBuilder buf = new StringBuilder(20);
if (theMillis > 0.0 && theMillis < 1.0) {
buf.append(getSubMillisecondMillisFormat().format(theMillis));
buf.append("ms");
} else if (theMillis < (10 * DateUtils.MILLIS_PER_SECOND)) {
buf.append((int) theMillis);
buf.append("ms");
} else if (theMillis >= DateUtils.MILLIS_PER_DAY) {
double days = theMillis / DateUtils.MILLIS_PER_DAY;
if (days >= 10) {
buf.append(getTenDayFormat().format(days));
buf.append(" days");
} else if (days != 1.0f) {
buf.append(getDayFormat().format(days));
buf.append(" days");
} else {
buf.append(getDayFormat().format(days));
buf.append(" day");
}
} else {
long millisAsLong = (long) theMillis;
append(buf, "", 2, ((millisAsLong % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR));
append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_HOUR) / DateUtils.MILLIS_PER_MINUTE));
append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_MINUTE) / DateUtils.MILLIS_PER_SECOND));
if (theMillis <= DateUtils.MILLIS_PER_MINUTE) {
append(buf, ".", 3, (millisAsLong % DateUtils.MILLIS_PER_SECOND));
}
}
return buf.toString();
}
private static long now() {
if (ourNowForUnitTest != null) {
return ourNowForUnitTest;
}
return System.currentTimeMillis();
}
@VisibleForTesting
static void setNowForUnitTestForUnitTest(Long theNowForUnitTest) {
ourNowForUnitTest = theNowForUnitTest;
}
} }

View File

@ -63,6 +63,7 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request. ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index. ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.forcedIdConstraintFailure=The operation has failed with a client-assigned ID constraint failure. This typically means that multiple client threads are trying to create a new resource with the same client-assigned ID at the same time, and this thread was chosen to be rejected.
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}" ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}"
@ -88,6 +89,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedIdNo
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidParameterChain=Invalid parameter chain: {0} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidParameterChain=Invalid parameter chain: {0}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidVersion=Version "{0}" is not valid for resource {1} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidVersion=Version "{0}" is not valid for resource {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.multipleParamsWithSameNameOneIsMissingTrue=This server does not know how to handle multiple "{0}" parameters where one has a value of :missing=true ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.multipleParamsWithSameNameOneIsMissingTrue=This server does not know how to handle multiple "{0}" parameters where one has a value of :missing=true
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.missingBody=No body was supplied in request
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed. ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulCreate=Successfully created resource "{0}" in {1}ms ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulCreate=Successfully created resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.i18n;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import java.util.Set; import java.util.Set;
@ -10,6 +11,15 @@ import org.junit.Test;
public class HapiLocalizerTest { public class HapiLocalizerTest {
@Test
public void testEscapePatterns() {
HapiLocalizer loc = new HapiLocalizer();
assertEquals("some message", loc.newMessageFormat("some message").format(new Object[]{}));
assertEquals("var1 {var2} var3 {var4}", loc.newMessageFormat("var1 {var2} var3 {var4}").format(new Object[]{}));
assertEquals("var1 A var3 B", loc.newMessageFormat("var1 {0} var3 {1}").format(new Object[]{ "A", "B"}));
}
@Test @Test
public void testAllKeys() { public void testAllKeys() {

View File

@ -0,0 +1,588 @@
package ca.uhn.fhir.interceptor.executor;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.StopWatch;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
public class InterceptorServiceTest {
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorServiceTest.class);
private List<String> myInvocations = new ArrayList<>();
@Test
public void testInterceptorWithAnnotationDefinedOnInterface() {
InterceptorService svc = new InterceptorService();
TestInterceptorWithAnnotationDefinedOnInterface_Class interceptor = new TestInterceptorWithAnnotationDefinedOnInterface_Class();
svc.registerInterceptor(interceptor);
assertEquals(1, interceptor.getRegisterCount());
}
@Test
public void testInterceptorThrowsException() {
class InterceptorThrowingException {
@Hook(Pointcut.TEST_RB)
public void test(String theValue) {
throw new AuthenticationException(theValue);
}
}
InterceptorService svc = new InterceptorService();
svc.registerInterceptor(new InterceptorThrowingException());
try {
svc.callHooks(Pointcut.TEST_RB, new HookParams("A MESSAGE", "B"));
fail();
} catch (AuthenticationException e) {
assertEquals("A MESSAGE", e.getMessage());
}
}
@Test
public void testInterceptorReturnsClass() {
class InterceptorReturningClass {
private BaseServerResponseException myNextResponse;
@Hook(Pointcut.TEST_RO)
public BaseServerResponseException hook() {
return myNextResponse;
}
}
InterceptorReturningClass interceptor0 = new InterceptorReturningClass();
InterceptorReturningClass interceptor1 = new InterceptorReturningClass();
InterceptorService svc = new InterceptorService();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
interceptor0.myNextResponse = new InvalidRequestException("0");
interceptor1.myNextResponse = new InvalidRequestException("1");
Object response = svc.callHooksAndReturnObject(Pointcut.TEST_RO, new HookParams("", ""));
assertEquals("0", ((InvalidRequestException) response).getMessage());
interceptor0.myNextResponse = null;
response = svc.callHooksAndReturnObject(Pointcut.TEST_RO, new HookParams("", ""));
assertEquals("1", ((InvalidRequestException) response).getMessage());
}
@Test
public void testRegisterHookFails() {
InterceptorService svc = new InterceptorService();
int initialSize = svc.getGlobalInterceptorsForUnitTest().size();
try {
svc.registerInterceptor(new InterceptorThatFailsOnRegister());
fail();
} catch (InternalErrorException e) {
// good
}
assertEquals(initialSize, svc.getGlobalInterceptorsForUnitTest().size());
}
@Test
public void testManuallyRegisterInterceptor() {
InterceptorService svc = new InterceptorService();
// Registered in opposite order to verify that the order on the annotation is used
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
svc.registerInterceptor(interceptor1);
svc.registerInterceptor(interceptor0);
// Register the manual interceptor (has Order right in the middle)
MyTestInterceptorManual myInterceptorManual = new MyTestInterceptorManual();
svc.registerInterceptor(myInterceptorManual);
List<Object> globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Try to register again (should have no effect
svc.registerInterceptor(myInterceptorManual);
globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Make sure we have the right invokers in the right order
List<Object> invokers = svc.getInterceptorsWithInvokersForPointcut(Pointcut.TEST_RB);
assertSame(interceptor0, invokers.get(0));
assertSame(myInterceptorManual, invokers.get(1));
assertSame(interceptor1, invokers.get(2));
// Finally, unregister it
svc.unregisterInterceptor(myInterceptorManual);
globalInterceptors = svc.getGlobalInterceptorsForUnitTest();
assertEquals(2, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo);
}
@Test
public void testInvokeGlobalInterceptorMethods() {
InterceptorService svc = new InterceptorService();
// Registered in opposite order to verify that the order on the annotation is used
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
svc.registerInterceptor(interceptor1);
svc.registerInterceptor(interceptor0);
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertTrue(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb", "MyTestInterceptorTwo.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame("A", interceptor1.myLastString0);
assertSame("B", interceptor1.myLastString1);
}
@Test
public void testInvokeUsingSupplierArg() {
InterceptorService svc = new InterceptorService();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertTrue(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb", "MyTestInterceptorTwo.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame("A", interceptor1.myLastString0);
assertSame("B", interceptor1.myLastString1);
}
@Test
public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() {
InterceptorService svc = new InterceptorService();
MyTestInterceptorOne interceptor0 = new MyTestInterceptorOne();
MyTestInterceptorTwo interceptor1 = new MyTestInterceptorTwo();
svc.registerInterceptor(interceptor0);
svc.registerInterceptor(interceptor1);
interceptor0.myNextReturn = false;
boolean outcome = svc.callHooks(Pointcut.TEST_RB, new HookParams("A", "B"));
assertFalse(outcome);
assertThat(myInvocations, contains("MyTestInterceptorOne.testRb"));
assertSame("A", interceptor0.myLastString0);
assertSame(null, interceptor1.myLastString0);
assertSame(null, interceptor1.myLastString1);
}
@Test
public void testCallHooksInvokedWithNullParameters() {
InterceptorService svc = new InterceptorService();
class NullParameterInterceptor {
private String myValue0 = "";
private String myValue1 = "";
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myValue0 = theValue0;
myValue1 = theValue1;
}
}
NullParameterInterceptor interceptor;
HookParams params;
// Both null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, null)
.add(String.class, null);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(null, interceptor.myValue0);
assertEquals(null, interceptor.myValue1);
svc.unregisterAllInterceptors();
// First null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, null)
.add(String.class, "A");
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(null, interceptor.myValue0);
assertEquals("A", interceptor.myValue1);
svc.unregisterAllInterceptors();
// Second null
interceptor = new NullParameterInterceptor();
svc.registerInterceptor(interceptor);
params = new HookParams()
.add(String.class, "A")
.add(String.class, null);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals("A", interceptor.myValue0);
assertEquals(null, interceptor.myValue1);
svc.unregisterAllInterceptors();
}
@Test
public void testCallHooksLogAndSwallowException() {
InterceptorService svc = new InterceptorService();
class LogAndSwallowInterceptor0 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new IllegalStateException();
}
}
LogAndSwallowInterceptor0 interceptor0 = new LogAndSwallowInterceptor0();
svc.registerInterceptor(interceptor0);
class LogAndSwallowInterceptor1 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new IllegalStateException();
}
}
LogAndSwallowInterceptor1 interceptor1 = new LogAndSwallowInterceptor1();
svc.registerInterceptor(interceptor1);
class LogAndSwallowInterceptor2 {
private boolean myHit;
@Hook(Pointcut.TEST_RB)
public void hook(String theValue0, String theValue1) {
myHit = true;
throw new NullPointerException("AAA");
}
}
LogAndSwallowInterceptor2 interceptor2 = new LogAndSwallowInterceptor2();
svc.registerInterceptor(interceptor2);
HookParams params = new HookParams()
.add(String.class, null)
.add(String.class, null);
try {
svc.callHooks(Pointcut.TEST_RB, params);
fail();
} catch (NullPointerException e) {
assertEquals("AAA", e.getMessage());
}
assertEquals(true, interceptor0.myHit);
assertEquals(true, interceptor1.myHit);
assertEquals(true, interceptor2.myHit);
}
@Test
public void testCallHooksInvokedWithWrongParameters() {
InterceptorService svc = new InterceptorService();
Integer msg = 123;
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
try {
svc.callHooks(Pointcut.TEST_RB, params);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("Invalid params for pointcut " + Pointcut.TEST_RB + " - Wanted java.lang.String,java.lang.String but found "));
}
}
@Test
public void testValidateParamTypes() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
params.add(String.class, "B");
boolean validated = svc.haveAppropriateParams(Pointcut.TEST_RB, params);
assertTrue(validated);
}
@Test
public void testValidateParamTypesMissingParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
try {
svc.haveAppropriateParams(Pointcut.TEST_RB, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut " + Pointcut.TEST_RB + " - Wanted java.lang.String,java.lang.String but found [String]", e.getMessage());
}
}
@Test
public void testValidateParamTypesExtraParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add(String.class, "A");
params.add(String.class, "B");
params.add(String.class, "C");
params.add(String.class, "D");
params.add(String.class, "E");
try {
svc.haveAppropriateParams(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut " + Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED + " - Wanted ca.uhn.fhir.rest.api.server.RequestDetails,ca.uhn.fhir.rest.server.servlet.ServletRequestDetails,org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [String, String, String, String, String]", e.getMessage());
}
}
@SuppressWarnings("unchecked")
@Test
public void testValidateParamTypesWrongParam() {
InterceptorService svc = new InterceptorService();
HookParams params = new HookParams();
params.add((Class) String.class, 1);
params.add((Class) String.class, 2);
params.add((Class) String.class, 3);
params.add((Class) String.class, 4);
try {
svc.haveAppropriateParams(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid params for pointcut " + Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED + " - class java.lang.Integer is not of type class java.lang.String", e.getMessage());
}
}
@Test
public void testThreadLocalHookInterceptor() {
InterceptorService svc = new InterceptorService();
svc.setThreadlocalInvokersEnabled(true);
HookParams params = new HookParams().add("A").add("B");
@Interceptor(order = 100)
class LocalInterceptor {
private int myCount = 0;
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0, String theString1) {
myCount++;
return true;
}
}
LocalInterceptor interceptor = new LocalInterceptor();
svc.registerThreadLocalInterceptor(interceptor);
try {
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(5, interceptor.myCount);
} finally {
svc.unregisterThreadLocalInterceptor(interceptor);
}
// Call some more - The interceptor is removed so the count shouldn't change
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
assertEquals(5, interceptor.myCount);
}
/**
* <pre>
* JA 20190321 On my MBP 2018
* ThreadLocalEnabled=true - Performed 500000 loops in 8383.0ms - 0.017ms / loop
* ThreadLocalEnabled=false - Performed 500000 loops in 3743.0ms - 0.007ms / loop
* ThreadLocalEnabled=true - Performed 500000 loops in 6163.0ms - 0.012ms / loop
* ThreadLocalEnabled=false - Performed 500000 loops in 3487.0ms - 0.007ms / loop
* ThreadLocalEnabled=true - Performed 1000000 loops in 00:00:12.458 - 0.012ms / loop
* ThreadLocalEnabled=false - Performed 1000000 loops in 7046.0ms - 0.007ms / loop
* </pre>
*/
@Test
@Ignore("Performance test - Not needed normally")
public void testThreadLocalHookInterceptorMicroBenchmark() {
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
threadLocalMicroBenchmark(true, 500000);
threadLocalMicroBenchmark(false, 500000);
}
private void threadLocalMicroBenchmark(boolean theThreadlocalInvokersEnabled, int theCount) {
InterceptorService svc = new InterceptorService();
svc.setThreadlocalInvokersEnabled(theThreadlocalInvokersEnabled);
HookParams params = new HookParams().add("A").add("B");
@Interceptor(order = 100)
class LocalInterceptor {
private int myCount = 0;
@Hook(Pointcut.TEST_RB)
public void testRb(String theString0, String theString1) {
myCount++;
}
}
LocalInterceptor interceptor = new LocalInterceptor();
StopWatch sw = new StopWatch();
for (int i = 0; i < theCount; i++) {
svc.registerThreadLocalInterceptor(interceptor);
try {
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
svc.callHooks(Pointcut.TEST_RB, params);
} finally {
svc.unregisterThreadLocalInterceptor(interceptor);
}
}
ourLog.info("ThreadLocalEnabled={} - Performed {} loops in {} - {} / loop - Outcomne: {}", theThreadlocalInvokersEnabled, theCount, sw.toString(), sw.formatMillisPerOperation(theCount), interceptor.myCount);
}
@Before
public void before() {
myInvocations.clear();
}
interface TestInterceptorWithAnnotationDefinedOnInterface_Interface {
@Hook(Pointcut.INTERCEPTOR_REGISTERED)
void registered();
}
@Interceptor(order = 100)
public class MyTestInterceptorOne {
private String myLastString0;
private boolean myNextReturn = true;
public MyTestInterceptorOne() {
super();
}
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0) {
myLastString0 = theString0;
myInvocations.add("MyTestInterceptorOne.testRb");
return myNextReturn;
}
}
@Interceptor(order = 300)
public class MyTestInterceptorTwo {
private String myLastString0;
private String myLastString1;
@Hook(Pointcut.TEST_RB)
public boolean testRb(String theString0, String theString1) {
myLastString0 = theString0;
myLastString1 = theString1;
myInvocations.add("MyTestInterceptorTwo.testRb");
return true;
}
}
@Interceptor(order = 200)
public class MyTestInterceptorManual {
@Hook(Pointcut.TEST_RB)
public void testRb() {
myInvocations.add("MyTestInterceptorManual.testRb");
}
}
public static class TestInterceptorWithAnnotationDefinedOnInterface_Class implements TestInterceptorWithAnnotationDefinedOnInterface_Interface {
private int myRegisterCount = 0;
public int getRegisterCount() {
return myRegisterCount;
}
@Override
public void registered() {
myRegisterCount++;
}
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class CanonicalSubscription {
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class ResourceDeliveryMessage {
}
@Interceptor()
public static class InterceptorThatFailsOnRegister {
@Hook(Pointcut.INTERCEPTOR_REGISTERED)
public void start() throws Exception {
throw new Exception("InterceptorThatFailsOnRegister FAILED!");
}
}
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.util;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
public class LogUtilTest {
@Test
public void testLevels() {
Logger log = mock(Logger.class);
LogUtil.log(log, Level.TRACE, "HELLO");
LogUtil.log(log, Level.DEBUG, "HELLO");
LogUtil.log(log, Level.INFO, "HELLO");
LogUtil.log(log, Level.WARN, "HELLO");
LogUtil.log(log, Level.ERROR, "HELLO");
verify(log, times(1)).trace(anyString(),any(Object[].class));
verify(log, times(1)).debug(anyString(),any(Object[].class));
verify(log, times(1)).info(anyString(),any(Object[].class));
verify(log, times(1)).warn(anyString(),any(Object[].class));
verify(log, times(1)).error(anyString(),any(Object[].class));
verifyNoMoreInteractions(log);
}
}

View File

@ -1,15 +1,27 @@
package ca.uhn.fhir.util; package ca.uhn.fhir.util;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class PortUtilTest { public class PortUtilTest {
private static final Logger ourLog = LoggerFactory.getLogger(PortUtilTest.class);
@Test @Test
public void testFindFreePort() throws IOException { public void testFindFreePort() throws IOException {
int port = PortUtil.findFreePort(); int port = PortUtil.findFreePort();
@ -28,4 +40,68 @@ public class PortUtilTest {
} }
} }
@After
public void after() {
PortUtil.setPortDelay(null);
}
@Test
public void testPortsAreNotReused() throws InterruptedException {
PortUtil.setPortDelay(0);
List<Integer> ports = Collections.synchronizedList(new ArrayList<>());
List<PortUtil> portUtils = Collections.synchronizedList(new ArrayList<>());
List<String> errors = Collections.synchronizedList(new ArrayList<>());
int tasksCount = 20;
ExecutorService pool = Executors.newFixedThreadPool(tasksCount);
int portsPerTaskCount = 151;
for (int i = 0; i < tasksCount; i++) {
pool.submit(() -> {
PortUtil portUtil = new PortUtil();
portUtils.add(portUtil);
for (int j = 0; j < portsPerTaskCount; j++) {
int nextFreePort = portUtil.getNextFreePort();
boolean bound;
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress("localhost", nextFreePort));
bound = true;
} catch (IOException e) {
bound = false;
}
if (!bound) {
try (ServerSocket ss = new ServerSocket()) {
Thread.sleep(1000);
ss.bind(new InetSocketAddress("localhost", nextFreePort));
} catch (Exception e) {
String msg = "Failure binding new port (second attempt) " + nextFreePort + ": " + e.toString();
ourLog.error(msg, e);
errors.add(msg);
}
}
ports.add(nextFreePort);
}
});
}
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
assertThat(errors.toString(), errors, empty());
assertEquals(tasksCount * portsPerTaskCount, ports.size());
while (ports.size() > 0) {
Integer nextPort = ports.remove(0);
if (ports.contains(nextPort)) {
fail("Port " + nextPort + " was given out more than once");
}
}
for (PortUtil next : portUtils) {
next.clearInstance();
}
}
} }

View File

@ -9,7 +9,8 @@ import java.util.Date;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class StopWatchTest { public class StopWatchTest {
@ -99,6 +100,7 @@ public class StopWatchTest {
@Test @Test
public void testFormatMillis() { public void testFormatMillis() {
assertEquals("0.134ms", StopWatch.formatMillis(0.1339d).replace(',', '.'));
assertEquals("1000ms", StopWatch.formatMillis(DateUtils.MILLIS_PER_SECOND)); assertEquals("1000ms", StopWatch.formatMillis(DateUtils.MILLIS_PER_SECOND));
assertEquals("00:01:00.000", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE)); assertEquals("00:01:00.000", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE));
assertEquals("00:01:01", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE + DateUtils.MILLIS_PER_SECOND)); assertEquals("00:01:01", StopWatch.formatMillis(DateUtils.MILLIS_PER_MINUTE + DateUtils.MILLIS_PER_SECOND));

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.StructureDefinition; import org.hl7.fhir.dstu3.model.StructureDefinition;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -56,6 +57,11 @@ public class LoadingValidationSupportDstu3 implements IValidationSupport {
return null; return null;
} }
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return null;
}
@Override @Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
String resName = myCtx.getResourceDefinition(theClass).getName(); String resName = myCtx.getResourceDefinition(theClass).getName();

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.r4.terminologies.ValueSetExpander; import org.hl7.fhir.r4.terminologies.ValueSetExpander;
@ -58,6 +59,11 @@ public class LoadingValidationSupportR4 implements org.hl7.fhir.r4.hapi.ctx.IVal
return null; return null;
} }
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return null;
}
@Override @Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
String resName = myCtx.getResourceDefinition(theClass).getName(); String resName = myCtx.getResourceDefinition(theClass).getName();

View File

@ -7,7 +7,6 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -59,8 +58,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -72,9 +72,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal; return retVal;
} }

View File

@ -8,12 +8,9 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -73,8 +70,9 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -86,9 +84,10 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal; return retVal;
} }

View File

@ -64,8 +64,9 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
/** /**
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
* @return
*/ */
public IServerInterceptor loggingInterceptor() { public LoggingInterceptor loggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -77,9 +78,10 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
/** /**
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected * This interceptor adds some pretty syntax highlighting in responses when a browser is detected
* @return
*/ */
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public IServerInterceptor responseHighlighterInterceptor() { public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
return retVal; return retVal;
} }

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -66,8 +67,8 @@ public class JpaServerDemo extends RestfulServer {
throw new IllegalStateException(); throw new IllegalStateException();
} }
List<IResourceProvider> beans = myAppCtx.getBean(resourceProviderBeanName, List.class); ResourceProviderFactory beans = myAppCtx.getBean(resourceProviderBeanName, ResourceProviderFactory.class);
setResourceProviders(beans); registerProviders(beans.createProviders());
/* /*
* The system provider implements non-resource-type methods, such as * The system provider implements non-resource-type methods, such as
@ -85,7 +86,7 @@ public class JpaServerDemo extends RestfulServer {
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
setPlainProviders(systemProvider); registerProvider(systemProvider);
/* /*
* The conformance provider exports the supported resources, search parameters, etc for * The conformance provider exports the supported resources, search parameters, etc for
@ -125,7 +126,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives * This server tries to dynamically generate narratives
*/ */
FhirContext ctx = getFhirContext(); FhirContext ctx = getFhirContext();
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext())); ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
/* /*
* Default to XML and pretty printing * Default to XML and pretty printing

View File

@ -21,6 +21,10 @@ package ca.uhn.fhir.rest.client.impl;
*/ */
import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.*;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.api.*;
@ -43,7 +47,11 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.instance.model.api.*;
import java.io.*; import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.*; import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
@ -65,13 +73,13 @@ public abstract class BaseClient implements IRestfulClient {
private final String myUrlBase; private final String myUrlBase;
private boolean myDontValidateConformance; private boolean myDontValidateConformance;
private EncodingEnum myEncoding = null; // default unspecified (will be XML) private EncodingEnum myEncoding = null; // default unspecified (will be XML)
private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>();
private boolean myKeepResponses = false; private boolean myKeepResponses = false;
private IHttpResponse myLastResponse; private IHttpResponse myLastResponse;
private String myLastResponseBody; private String myLastResponseBody;
private Boolean myPrettyPrint = false; private Boolean myPrettyPrint = false;
private SummaryEnum mySummary; private SummaryEnum mySummary;
private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT; private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT;
private IInterceptorService myInterceptorService;
BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) {
super(); super();
@ -92,6 +100,18 @@ public abstract class BaseClient implements IRestfulClient {
myEncoding = EncodingEnum.JSON; myEncoding = EncodingEnum.JSON;
} }
setInterceptorService(new InterceptorService());
}
@Override
public IInterceptorService getInterceptorService() {
return myInterceptorService;
}
@Override
public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) {
Validate.notNull(theInterceptorService, "theInterceptorService must not be null");
myInterceptorService = theInterceptorService;
} }
protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) { protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) {
@ -149,14 +169,6 @@ public abstract class BaseClient implements IRestfulClient {
return myClient; return myClient;
} }
/**
* {@inheritDoc}
*/
@Override
public List<IClientInterceptor> getInterceptors() {
return Collections.unmodifiableList(myInterceptors);
}
/** /**
* For now, this is a part of the internal API of HAPI - Use with caution as this method may change! * For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
*/ */
@ -276,15 +288,16 @@ public abstract class BaseClient implements IRestfulClient {
} }
} }
for (IClientInterceptor nextInterceptor : myInterceptors) { HookParams requestParams = new HookParams();
nextInterceptor.interceptRequest(httpRequest); requestParams.add(IHttpRequest.class, httpRequest);
} getInterceptorService().callHooks(Pointcut.CLIENT_REQUEST, requestParams);
response = httpRequest.execute(); response = httpRequest.execute();
for (IClientInterceptor nextInterceptor : myInterceptors) { HookParams responseParams = new HookParams();
nextInterceptor.interceptResponse(response); responseParams.add(IHttpRequest.class, httpRequest);
} responseParams.add(IHttpResponse.class, response);
getInterceptorService().callHooks(Pointcut.CLIENT_RESPONSE, responseParams);
String mimeType; String mimeType;
if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) { if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) {
@ -446,7 +459,7 @@ public abstract class BaseClient implements IRestfulClient {
@Override @Override
public void registerInterceptor(IClientInterceptor theInterceptor) { public void registerInterceptor(IClientInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null"); Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.add(theInterceptor); getInterceptorService().registerInterceptor(theInterceptor);
} }
/** /**
@ -461,7 +474,7 @@ public abstract class BaseClient implements IRestfulClient {
@Override @Override
public void unregisterInterceptor(IClientInterceptor theInterceptor) { public void unregisterInterceptor(IClientInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null"); Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor); getInterceptorService().unregisterInterceptor(theInterceptor);
} }
protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> { protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> {

View File

@ -272,10 +272,9 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
@Override @Override
public void validateServerBase(String theServerBase, IHttpClient theHttpClient, IRestfulClient theClient) { public void validateServerBase(String theServerBase, IHttpClient theHttpClient, IRestfulClient theClient) {
GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this); GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
client.setInterceptorService(theClient.getInterceptorService());
client.setEncoding(theClient.getEncoding()); client.setEncoding(theClient.getEncoding());
for (IClientInterceptor interceptor : theClient.getInterceptors()) {
client.registerInterceptor(interceptor);
}
client.setDontValidateConformance(true); client.setDontValidateConformance(true);
IBaseResource conformance; IBaseResource conformance;

View File

@ -67,6 +67,11 @@ public class IgPackValidationSupportDstu3 implements IValidationSupport {
return fetchResource(theContext, CodeSystem.class, theSystem); return fetchResource(theContext, CodeSystem.class, theSystem);
} }
@Override
public ValueSet fetchValueSet(FhirContext theContext, String theSystem) {
return fetchResource(theContext, ValueSet.class, theSystem);
}
@Override @Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
for (Map.Entry<IIdType, IBaseResource> next : myIgResources.entrySet()) { for (Map.Entry<IIdType, IBaseResource> next : myIgResources.entrySet()) {

View File

@ -156,10 +156,10 @@ public abstract class AbstractJaxRsBundleProvider extends AbstractJaxRsProvider
/** /**
* Default: an empty list of interceptors * Default: an empty list of interceptors
* *
* @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors() * @see ca.uhn.fhir.rest.server.IRestfulServerDefaults#getInterceptors_()
*/ */
@Override @Override
public List<IServerInterceptor> getInterceptors() { public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -33,6 +33,7 @@ import javax.ws.rs.*;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@ -25,6 +25,7 @@ import java.util.Map.Entry;
import javax.ws.rs.core.*; import javax.ws.rs.core.*;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -74,6 +75,11 @@ public abstract class AbstractJaxRsProvider implements IRestfulServerDefaults {
CTX = ctx; CTX = ctx;
} }
@Override
public IInterceptorService getInterceptorService() {
return null;
}
/** /**
* DEFAULT = AddProfileTagEnum.NEVER * DEFAULT = AddProfileTagEnum.NEVER
*/ */
@ -145,10 +151,10 @@ public abstract class AbstractJaxRsProvider implements IRestfulServerDefaults {
* Default: an empty list of interceptors (Interceptors are not yet supported * Default: an empty list of interceptors (Interceptors are not yet supported
* in the JAX-RS server). Please get in touch if you'd like to help! * in the JAX-RS server). Please get in touch if you'd like to help!
* *
* @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors() * @see ca.uhn.fhir.rest.server.IRestfulServerDefaults#getInterceptors_()
*/ */
@Override @Override
public List<IServerInterceptor> getInterceptors() { public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -28,6 +28,7 @@ import javax.ws.rs.*;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;

View File

@ -67,6 +67,7 @@ public class JaxRsRequest extends RequestDetails {
*/ */
public JaxRsRequest(AbstractJaxRsProvider server, String resourceString, RequestTypeEnum requestType, public JaxRsRequest(AbstractJaxRsProvider server, String resourceString, RequestTypeEnum requestType,
RestOperationTypeEnum restOperation) { RestOperationTypeEnum restOperation) {
super(server.getInterceptorService());
this.myHeaders = server.getHeaders(); this.myHeaders = server.getHeaders();
this.myResourceString = resourceString; this.myResourceString = resourceString;
this.setRestOperationType(restOperation); this.setRestOperationType(restOperation);

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jaxrs.server.test; package ca.uhn.fhir.jaxrs.server.test;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -18,4 +19,5 @@ public class TestJaxRsDummyPatientProviderDstu3 extends AbstractJaxRsResourcePro
public Class<Patient> getResourceType() { public Class<Patient> getResourceType() {
return Patient.class; return Patient.class;
} }
} }

View File

@ -127,7 +127,7 @@ public class JaxRsPatientRestProvider extends AbstractJaxRsResourceProvider<Pati
/** THE DEFAULTS */ /** THE DEFAULTS */
@Override @Override
public List<IServerInterceptor> getInterceptors() { public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -128,7 +128,7 @@ public class JaxRsPatientRestProviderDstu3 extends AbstractJaxRsResourceProvider
/** THE DEFAULTS */ /** THE DEFAULTS */
@Override @Override
public List<IServerInterceptor> getInterceptors() { public List<IServerInterceptor> getInterceptors_() {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -126,23 +126,16 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>net.ttddyy</groupId>
<artifactId>jcl-over-slf4j</artifactId> <artifactId>datasource-proxy</artifactId>
</dependency> </dependency>
-->
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.javassist</groupId> <groupId>org.javassist</groupId>
@ -154,6 +147,10 @@
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId> <artifactId>jackson-annotations</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.helger</groupId> <groupId>com.helger</groupId>
@ -189,14 +186,8 @@
<!-- Patch Dependencies --> <!-- Patch Dependencies -->
<dependency> <dependency>
<groupId>net.riotopsys</groupId> <groupId>com.github.java-json-tools</groupId>
<artifactId>json_patch</artifactId> <artifactId>json-patch</artifactId>
<exclusions>
<exclusion>
<artifactId>com.google.code.gson</artifactId>
<groupId>gson</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.dnault</groupId> <groupId>com.github.dnault</groupId>
@ -460,6 +451,10 @@
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
@ -523,11 +518,6 @@
<artifactId>greenmail-spring</artifactId> <artifactId>greenmail-spring</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.github.ben-manes.caffeine</groupId> <groupId>com.github.ben-manes.caffeine</groupId>

View File

@ -2,6 +2,8 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@ -14,15 +16,16 @@ import ca.uhn.fhir.jpa.subscription.module.cache.ISubscribableChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.cache.LinkedBlockingQueueSubscribableChannelFactory; import ca.uhn.fhir.jpa.subscription.module.cache.LinkedBlockingQueueSubscribableChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.jpa.util.JpaInterceptorService;
import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.annotation.SchedulingConfigurer;
@ -71,11 +74,17 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Autowired @Autowired
protected Environment myEnv; protected Environment myEnv;
@Override @Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) { public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) {
theTaskRegistrar.setTaskScheduler(taskScheduler()); theTaskRegistrar.setTaskScheduler(taskScheduler());
} }
@Bean("myDaoRegistry")
public DaoRegistry daoRegistry() {
return new DaoRegistry();
}
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)
public DatabaseBackedPagingProvider databaseBackedPagingProvider() { public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
return new DatabaseBackedPagingProvider(); return new DatabaseBackedPagingProvider();
@ -158,13 +167,36 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new CompositeInMemoryDaoSubscriptionMatcher(daoSubscriptionMatcher(), inMemorySubscriptionMatcher()); return new CompositeInMemoryDaoSubscriptionMatcher(daoSubscriptionMatcher(), inMemorySubscriptionMatcher());
} }
@Bean
public HapiFhirHibernateJpaDialect hibernateJpaDialect() {
return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer());
}
@Bean
public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
@Bean
public IInterceptorService jpaInterceptorService() {
return new JpaInterceptorService();
}
/**
* Subclasses may override
*/
protected boolean isSupported(String theResourceType) {
return daoRegistry().getResourceDaoIfExists(theResourceType) != null;
}
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) { public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer())); theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity"); theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");
theFactory.setPersistenceProvider(new HibernatePersistenceProvider()); theFactory.setPersistenceProvider(new HibernatePersistenceProvider());
} }
private static HibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) { private static HapiFhirHibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) {
return new HapiFhirHibernateJpaDialect(theLocalizer); return new HapiFhirHibernateJpaDialect(theLocalizer);
} }
} }

View File

@ -21,20 +21,28 @@ package ca.uhn.fhir.jpa.config;
*/ */
import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.StaleStateException;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.mapping.PreferredConstructor; import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import javax.persistence.PersistenceException;
import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect { public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
private static final Logger ourLog = LoggerFactory.getLogger(HapiFhirHibernateJpaDialect.class);
private HapiLocalizer myLocalizer; private HapiLocalizer myLocalizer;
/** /**
@ -44,18 +52,56 @@ public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
myLocalizer = theLocalizer; myLocalizer = theLocalizer;
} }
public RuntimeException translate(PersistenceException theException, String theMessageToPrepend) {
if (theException.getCause() instanceof HibernateException) {
return new PersistenceException(convertHibernateAccessException((HibernateException) theException.getCause(), theMessageToPrepend));
}
return theException;
}
@Override @Override
protected DataAccessException convertHibernateAccessException(HibernateException theException) { protected DataAccessException convertHibernateAccessException(HibernateException theException) {
return convertHibernateAccessException(theException, null);
}
private DataAccessException convertHibernateAccessException(HibernateException theException, String theMessageToPrepend) {
String messageToPrepend = "";
if (isNotBlank(theMessageToPrepend)) {
messageToPrepend = theMessageToPrepend + " - ";
}
if (theException instanceof ConstraintViolationException) { if (theException instanceof ConstraintViolationException) {
String constraintName = ((ConstraintViolationException) theException).getConstraintName(); String constraintName = ((ConstraintViolationException) theException).getConstraintName();
switch (defaultString(constraintName)) { switch (defaultString(constraintName)) {
case ResourceHistoryTable.IDX_RESVER_ID_VER: case ResourceHistoryTable.IDX_RESVER_ID_VER:
throw new ResourceVersionConflictException(myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure")); throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"));
case ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING: case ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING:
throw new ResourceVersionConflictException(myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure")); throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure"));
case ForcedId.IDX_FORCEDID_TYPE_FID:
throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "forcedIdConstraintFailure"));
} }
} }
/*
* It would be nice if we could be more precise here, since technically any optimistic lock
* failure could result in a StaleStateException, but with the error message we're returning
* we're basically assuming it's an optimistic lock failure on HFJ_RESOURCE.
*
* That said, I think this is an OK trade-off. There is a high probability that if this happens
* it is a failure on HFJ_RESOURCE (there aren't many other tables in our schema that
* use @Version at all) and this error message is infinitely more comprehensible
* than the one we'd otherwise return.
*
* The actual StaleStateException is thrown in hibernate's Expectations
* class in a method called "checkBatched" currently. This can all be tested using the
* StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction()
*/
if (theException instanceof StaleStateException) {
String msg = messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure");
throw new ResourceVersionConflictException(msg);
}
return super.convertHibernateAccessException(theException); return super.convertHibernateAccessException(theException);
} }

View File

@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.config;
* #L% * #L%
*/ */
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionWebsocketHandler; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.subscription.module.subscriber.websocket.SubscriptionWebsocketHandler;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@ -35,10 +37,12 @@ import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
@EnableWebSocket() @EnableWebSocket()
@Controller @Controller
public class WebsocketDispatcherConfig implements WebSocketConfigurer { public class WebsocketDispatcherConfig implements WebSocketConfigurer {
@Autowired
ModelConfig myModelConfig;
@Override @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) { public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) {
theRegistry.addHandler(subscriptionWebSocketHandler(), "/websocket").setAllowedOrigins("*"); theRegistry.addHandler(subscriptionWebSocketHandler(), myModelConfig.getWebsocketContextPath()).setAllowedOrigins("*");
} }
@Bean(autowire = Autowire.BY_TYPE) @Bean(autowire = Autowire.BY_TYPE)

View File

@ -1,15 +1,16 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.*;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
@ -130,8 +131,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
@Autowired @Autowired
protected IdHelperService myIdHelperService; protected IdHelperService myIdHelperService;
@Autowired @Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IForcedIdDao myForcedIdDao; protected IForcedIdDao myForcedIdDao;
@Autowired @Autowired
protected ISearchResultDao mySearchResultDao; protected ISearchResultDao mySearchResultDao;
@ -192,6 +191,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
private SearchBuilderFactory mySearchBuilderFactory; private SearchBuilderFactory mySearchBuilderFactory;
private ApplicationContext myApplicationContext; private ApplicationContext myApplicationContext;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
/** /**
* Returns the newly created forced ID. If the entity already had a forced ID, or if * Returns the newly created forced ID. If the entity already had a forced ID, or if
@ -223,6 +224,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
throw new MethodNotAllowedException("$expunge is not enabled on this server"); throw new MethodNotAllowedException("$expunge is not enabled on this server");
} }
if (theExpungeOptions.getLimit() < 1) {
throw new InvalidRequestException("Expunge limit may not be less than 1. Received expunge limit " + theExpungeOptions.getLimit() + ".");
}
AtomicInteger remainingCount = new AtomicInteger(theExpungeOptions.getLimit()); AtomicInteger remainingCount = new AtomicInteger(theExpungeOptions.getLimit());
if (theResourceName == null && theResourceId == null && theVersion == null) { if (theResourceName == null && theResourceId == null && theVersion == null) {
@ -274,12 +279,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
for (Long next : resourceIds) { for (Long next : resourceIds) {
txTemplate.execute(t -> { txTemplate.execute(t -> {
expungeHistoricalVersionsOfId(next, remainingCount); expungeHistoricalVersionsOfId(next, remainingCount);
return null;
});
if (remainingCount.get() <= 0) { if (remainingCount.get() <= 0) {
ourLog.debug("Expunge limit has been hit - Stopping operation"); ourLog.debug("Expunge limit has been hit - Stopping operation");
return toExpungeOutcome(theExpungeOptions, remainingCount); return toExpungeOutcome(theExpungeOptions, remainingCount);
} }
return null;
});
} }
/* /*
@ -290,6 +295,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
expungeCurrentVersionOfResource(next, remainingCount); expungeCurrentVersionOfResource(next, remainingCount);
return null; return null;
}); });
if (remainingCount.get() <= 0) {
ourLog.debug("Expunge limit has been hit - Stopping operation");
return toExpungeOutcome(theExpungeOptions, remainingCount);
}
} }
} }
@ -301,8 +310,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/ */
Pageable page = PageRequest.of(0, remainingCount.get()); Pageable page = PageRequest.of(0, remainingCount.get());
Slice<Long> historicalIds = txTemplate.execute(t -> { Slice<Long> historicalIds = txTemplate.execute(t -> {
if (theResourceId != null && theVersion != null) { if (theResourceId != null) {
if (theVersion != null) {
return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion)); return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion));
} else {
return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theResourceId);
}
} else { } else {
if (theResourceName != null) { if (theResourceName != null) {
return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName); return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName);
@ -611,10 +624,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return myDaoRegistry.getResourceDaoIfExists(theType); return myDaoRegistry.getResourceDaoIfExists(theType);
} }
protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
return myDaoRegistry.getDaoOrThrowException(theClass);
}
protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) { if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
return null; return null;
@ -771,10 +780,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) { if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) {
theRequestDetails.notifyIncomingRequestPreHandled(theOperationType); theRequestDetails.notifyIncomingRequestPreHandled(theOperationType);
} }
List<IServerInterceptor> interceptors = getConfig().getInterceptors();
for (IServerInterceptor next : interceptors) {
next.incomingRequestPreHandled(theOperationType, theRequestDetails);
}
} }
/** /**
@ -1306,7 +1311,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams); mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true); changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
if (changed.isChanged()) {
theEntity.setUpdated(theUpdateTime); theEntity.setUpdated(theUpdateTime);
if (theResource instanceof IResource) { if (theResource instanceof IResource) {
theEntity.setLanguage(((IResource) theResource).getLanguage().getValue()); theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
@ -1317,6 +1322,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
newParams.setParamsOn(theEntity); newParams.setParamsOn(theEntity);
theEntity.setIndexStatus(INDEX_STATUS_INDEXED); theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
populateFullTextFields(myContext, theResource, theEntity); populateFullTextFields(myContext, theResource, theEntity);
}
} else { } else {
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false); changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
@ -1425,6 +1431,11 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion, public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) { ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) {
// We'll update the resource ID with the correct version later but for
// now at least set it to something useful for the interceptors
theResource.setId(theEntity.getIdDt());
// Notify interceptors // Notify interceptors
ActionRequestDetails requestDetails; ActionRequestDetails requestDetails;
if (theRequestDetails != null) { if (theRequestDetails != null) {
@ -1433,18 +1444,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
// Notify IServerOperationInterceptors about pre-action call // Notify IServerOperationInterceptors about pre-action call
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourcePreUpdate(theOldResource, theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
}
}
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource) .add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource); .add(IBaseResource.class, theResource)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams); .add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams);
// Perform update // Perform update
ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing); ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
@ -1466,23 +1471,15 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
// Notify interceptors // Notify interceptors
if (!savedEntity.isUnchangedInCurrentOperation()) { if (!savedEntity.isUnchangedInCurrentOperation()) {
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
theRequestDetails.getRequestOperationCallback().resourceUpdated(theOldResource, theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override @Override
public void beforeCommit(boolean readOnly) { public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource) .add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource); .add(IBaseResource.class, theResource)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams); .add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
} }
}); });
} }
@ -1550,6 +1547,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
continue; continue;
} }
if (getConfig().isEnforceReferenceTargetTypes()) {
for (IBase nextChild : values) { for (IBase nextChild : values) {
IBaseReference nextRef = (IBaseReference) nextChild; IBaseReference nextRef = (IBaseReference) nextChild;
IIdType referencedId = nextRef.getReferenceElement(); IIdType referencedId = nextRef.getReferenceElement();
@ -1564,6 +1562,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
} }
} }
}
} }
} }

View File

@ -24,10 +24,11 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
@ -51,6 +52,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*; import ca.uhn.fhir.util.*;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.instance.model.api.*;
@ -70,6 +72,7 @@ import javax.annotation.PostConstruct;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*; import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -141,6 +144,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override @Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTimestamp, RequestDetails theRequestDetails) { public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTimestamp, RequestDetails theRequestDetails) {
if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody");
throw new InvalidRequestException(msg);
}
if (isNotBlank(theResource.getIdElement().getIdPart())) { if (isNotBlank(theResource.getIdElement().getIdPart())) {
if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart()); String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
@ -212,14 +220,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, null, false); T resourceToDelete = toResource(myResourceType, entity, null, false);
// Notify IServerOperationInterceptors about pre-action call // Notify IServerOperationInterceptors about pre-action call
if (theRequest != null) { HookParams hook = new HookParams()
theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete); .add(IBaseResource.class, resourceToDelete)
} .add(RequestDetails.class, theRequest)
for (IServerInterceptor next : getConfig().getInterceptors()) { .addIfMatchesType(ServletRequestDetails.class, theRequest);
if (next instanceof IServerOperationInterceptor) { myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook);
((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
}
}
validateOkToDelete(theDeleteConflicts, entity, false); validateOkToDelete(theDeleteConflicts, entity, false);
@ -236,25 +241,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceToDelete.setId(entity.getIdDt()); resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors // Notify JPA interceptors
if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId);
theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override @Override
public void beforeCommit(boolean readOnly) { public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete); .add(IBaseResource.class, resourceToDelete)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams); .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
} }
}); });
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resourceToDelete).setCreated(true);
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis()); String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
@ -302,14 +300,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, null, false); T resourceToDelete = toResource(myResourceType, entity, null, false);
// Notify IServerOperationInterceptors about pre-action call // Notify IServerOperationInterceptors about pre-action call
if (theRequest != null) { HookParams hooks = new HookParams()
theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete); .add(IBaseResource.class, resourceToDelete)
} .add(RequestDetails.class, theRequest)
for (IServerInterceptor next : getConfig().getInterceptors()) { .addIfMatchesType(ServletRequestDetails.class, theRequest);
if (next instanceof IServerOperationInterceptor) { myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
}
}
validateOkToDelete(deleteConflicts, entity, false); validateOkToDelete(deleteConflicts, entity, false);
@ -326,21 +321,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceToDelete.setId(entity.getIdDt()); resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors // Notify JPA interceptors
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override @Override
public void beforeCommit(boolean readOnly) { public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete); .add(IBaseResource.class, resourceToDelete)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams); .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
} }
}); });
} }
@ -404,7 +392,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity = myEntityManager.find(ResourceTable.class, pid); entity = myEntityManager.find(ResourceTable.class, pid);
IBaseResource resource = toResource(entity, false); IBaseResource resource = toResource(entity, false);
theResource.setId(resource.getIdElement().getValue()); theResource.setId(resource.getIdElement().getValue());
return toMethodOutcome(entity, resource).setCreated(false); return toMethodOutcome(theRequest, entity, resource).setCreated(false);
} }
} }
@ -437,17 +425,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
// Notify JPA interceptors // Notify JPA interceptors
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourcePreCreate(theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource);
}
}
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource); .add(IBaseResource.class, theResource)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, hookParams); .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
// Perform actual DB update // Perform actual DB update
ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
@ -482,25 +464,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify JPA interceptors // Notify JPA interceptors
if (!updatedEntity.isUnchangedInCurrentOperation()) { if (!updatedEntity.isUnchangedInCurrentOperation()) {
if (theRequest != null) {
theRequest.getRequestOperationCallback().resourceCreated(theResource);
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceCreated(theRequest, theResource);
}
}
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override @Override
public void beforeCommit(boolean readOnly) { public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource); .add(IBaseResource.class, theResource)
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, hookParams); .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
} }
}); });
}
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true);
if (!thePerformIndexing) { if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement()); outcome.setId(theResource.getIdElement());
} }
@ -919,7 +895,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override @Override
public T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) { public T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) {
validateResourceTypeAndThrowIllegalArgumentException(theId); validateResourceTypeAndThrowInvalidRequestException(theId);
// Notify interceptors // Notify interceptors
if (theRequestDetails != null) { if (theRequestDetails != null) {
@ -941,8 +917,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED // Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
HookParams params = new HookParams().add(IBaseResource.class, retVal); HookParams params = new HookParams()
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params); .add(IBaseResource.class, retVal)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCE, params);
ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal; return retVal;
@ -956,7 +935,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override @Override
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) { public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
validateResourceTypeAndThrowIllegalArgumentException(theId); validateResourceTypeAndThrowInvalidRequestException(theId);
Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart()); Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid); BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
@ -1002,6 +981,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theId); throw new ResourceNotFoundException(theId);
} }
validateGivenIdIsAppropriateToRetrieveResource(theId, entity); validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
entity.setTransientForcedId(theId.getIdPart());
return entity; return entity;
} }
@ -1071,7 +1051,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) { public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
for (List<List<? extends IQueryParameterType>> nextAnds : theParams.values()) { for (List<List<IQueryParameterType>> nextAnds : theParams.values()) {
for (List<? extends IQueryParameterType> nextOrs : nextAnds) { for (List<? extends IQueryParameterType> nextOrs : nextAnds) {
for (IQueryParameterType next : nextOrs) { for (IQueryParameterType next : nextOrs) {
if (next.getMissing() != null) { if (next.getMissing() != null) {
@ -1131,10 +1111,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
HashSet<Long> retVal = new HashSet<Long>(); HashSet<Long> retVal = new HashSet<Long>();
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
Iterator<Long> iter = builder.createQuery(theParams, uuid); SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(uuid);
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails)) {
while (iter.hasNext()) { while (iter.hasNext()) {
retVal.add(iter.next()); retVal.add(iter.next());
} }
} catch (IOException e) {
ourLog.error("IO failure during database access", e);
}
return retVal; return retVal;
} }
@ -1174,7 +1159,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal; return retVal;
} }
private DaoMethodOutcome toMethodOutcome(@Nonnull final ResourceTable theEntity, @Nonnull IBaseResource theResource) { private DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final ResourceTable theEntity, @Nonnull IBaseResource theResource) {
DaoMethodOutcome outcome = new DaoMethodOutcome(); DaoMethodOutcome outcome = new DaoMethodOutcome();
IIdType id = null; IIdType id = null;
@ -1192,9 +1177,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
outcome.setResource(theResource); outcome.setResource(theResource);
outcome.setEntity(theEntity); outcome.setEntity(theEntity);
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED // Interceptor broadcast
HookParams params = new HookParams().add(IBaseResource.class, theResource); HookParams params = new HookParams()
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params); .add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCE, params);
return outcome; return outcome;
} }
@ -1266,6 +1254,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) {
if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody");
throw new InvalidRequestException(msg);
}
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource); preProcessResourceForStorage(theResource);
@ -1333,7 +1326,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
*/ */
if (!thePerformIndexing) { if (!thePerformIndexing) {
theResource.setId(entity.getIdDt().getValue()); theResource.setId(entity.getIdDt().getValue());
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(false); DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, entity, theResource).setCreated(false);
outcome.setPreviousResource(oldResource); outcome.setPreviousResource(oldResource);
return outcome; return outcome;
} }
@ -1342,7 +1335,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
* Otherwise, we're not in a transaction * Otherwise, we're not in a transaction
*/ */
ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource); ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource);
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false); DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, theResource).setCreated(false);
if (!thePerformIndexing) { if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement()); outcome.setId(theResource.getIdElement());
@ -1427,9 +1420,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
validateResourceType(entity, myResourceName); validateResourceType(entity, myResourceName);
} }
private void validateResourceTypeAndThrowIllegalArgumentException(IIdType theId) { private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) {
if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) { if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
throw new IllegalArgumentException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName); // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database exception
throw new InvalidRequestException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
} }
} }

View File

@ -114,7 +114,6 @@ public class DaoConfig {
* update setter javadoc if default changes * update setter javadoc if default changes
*/ */
private boolean myIndexContainedResources = true; private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors = new ArrayList<>();
/** /**
* update setter javadoc if default changes * update setter javadoc if default changes
*/ */
@ -142,6 +141,7 @@ public class DaoConfig {
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>(); private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches; private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true; private boolean myEnableInMemorySubscriptionMatching = true;
private boolean myEnforceReferenceTargetTypes = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC; private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/** /**
@ -161,6 +161,26 @@ public class DaoConfig {
} }
} }
/**
* If set to <code>true</code> (default is true) when a resource is being persisted,
* the target resource types of references will be validated to ensure that they
* are appropriate for the field containing the reference. This is generally a good idea
* because invalid reference target types may not be searchable.
*/
public boolean isEnforceReferenceTargetTypes() {
return myEnforceReferenceTargetTypes;
}
/**
* If set to <code>true</code> (default is true) when a resource is being persisted,
* the target resource types of references will be validated to ensure that they
* are appropriate for the field containing the reference. This is generally a good idea
* because invalid reference target types may not be searchable.
*/
public void setEnforceReferenceTargetTypes(boolean theEnforceReferenceTargetTypes) {
myEnforceReferenceTargetTypes = theEnforceReferenceTargetTypes;
}
/** /**
* If a non-null value is supplied (default is <code>null</code>), a default * If a non-null value is supplied (default is <code>null</code>), a default
* for the <code>_total</code> parameter may be specified here. For example, * for the <code>_total</code> parameter may be specified here. For example,
@ -506,47 +526,6 @@ public class DaoConfig {
myIndexMissingFieldsEnabled = theIndexMissingFields; myIndexMissingFieldsEnabled = theIndexMissingFields;
} }
/**
* Returns the interceptors which will be notified of operations.
*
* @see #setInterceptors(List)
* @deprecated Marked as deprecated as of HAPI 3.7.0. Use {@link #registerInterceptor} or {@link #unregisterInterceptor}instead.
*/
@Deprecated
public List<IServerInterceptor> getInterceptors() {
return myInterceptors;
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(List<IServerInterceptor> theInterceptors) {
myInterceptors = theInterceptors;
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(IServerInterceptor... theInterceptor) {
setInterceptors(new ArrayList<>());
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
}
public void registerInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
if (!myInterceptors.contains(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
/** /**
* See {@link #setMaximumExpansionSize(int)} * See {@link #setMaximumExpansionSize(int)}
*/ */
@ -1392,6 +1371,7 @@ public class DaoConfig {
/** /**
* If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions * If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions
* and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs. * and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs.
*
* @since 3.7.0 * @since 3.7.0
*/ */
@ -1402,6 +1382,7 @@ public class DaoConfig {
/** /**
* If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions * If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions
* and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs. * and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs.
*
* @since 3.7.0 * @since 3.7.0
*/ */
@ -1557,6 +1538,21 @@ public class DaoConfig {
myModelConfig.setEmailFromAddress(theEmailFromAddress); myModelConfig.setEmailFromAddress(theEmailFromAddress);
} }
/**
* If websocket subscriptions are enabled, this defines the context path that listens to them. Default value "/websocket".
*/
public String getWebsocketContextPath() {
return myModelConfig.getWebsocketContextPath();
}
/**
* If websocket subscriptions are enabled, this defines the context path that listens to them. Default value "/websocket".
*/
public void setWebsocketContextPath(String theWebsocketContextPath) {
myModelConfig.setWebsocketContextPath(theWebsocketContextPath);
}
public enum IndexEnabledEnum { public enum IndexEnabledEnum {
ENABLED, ENABLED,

View File

@ -32,21 +32,36 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Collection; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Component("myDaoRegistry")
public class DaoRegistry implements ApplicationContextAware { public class DaoRegistry implements ApplicationContextAware {
private ApplicationContext myAppCtx; private ApplicationContext myAppCtx;
@Autowired @Autowired
private FhirContext myContext; private FhirContext myContext;
/**
* Constructor
*/
public DaoRegistry() {
super();
}
private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao; private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao;
private volatile IFhirSystemDao<?, ?> mySystemDao; private volatile IFhirSystemDao<?, ?> mySystemDao;
private Set<String> mySupportedResourceTypes;
public void setSupportedResourceTypes(Collection<String> theSupportedResourceTypes) {
HashSet<String> supportedResourceTypes = new HashSet<>();
if (theSupportedResourceTypes != null) {
supportedResourceTypes.addAll(theSupportedResourceTypes);
}
mySupportedResourceTypes = supportedResourceTypes;
myResourceNameToResourceDao = null;
}
@Override @Override
public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException { public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
@ -62,6 +77,9 @@ public class DaoRegistry implements ApplicationContextAware {
return retVal; return retVal;
} }
/**
* @throws InvalidRequestException If the given resource type is not supported
*/
public IFhirResourceDao getResourceDao(String theResourceName) { public IFhirResourceDao getResourceDao(String theResourceName) {
init(); init();
IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName); IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName);
@ -84,7 +102,19 @@ public class DaoRegistry implements ApplicationContextAware {
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(Class<T> theResourceType) { public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(Class<T> theResourceType) {
String resourceName = myContext.getResourceDefinition(theResourceType).getName(); String resourceName = myContext.getResourceDefinition(theResourceType).getName();
try {
return (IFhirResourceDao<T>) getResourceDao(resourceName); return (IFhirResourceDao<T>) getResourceDao(resourceName);
} catch (InvalidRequestException e) {
return null;
}
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(String theResourceType) {
try {
return (IFhirResourceDao<T>) getResourceDao(theResourceType);
} catch (InvalidRequestException e) {
return null;
}
} }
private void init() { private void init() {
@ -103,9 +133,11 @@ public class DaoRegistry implements ApplicationContextAware {
for (IFhirResourceDao nextResourceDao : theResourceDaos) { for (IFhirResourceDao nextResourceDao : theResourceDaos) {
RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType()); RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType());
if (mySupportedResourceTypes == null || mySupportedResourceTypes.contains(nextResourceDef.getName())) {
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao); myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
} }
} }
}
public IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) { public IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao retVal = getResourceDao(theClass); IFhirResourceDao retVal = getResourceDao(theClass);
@ -128,4 +160,16 @@ public class DaoRegistry implements ApplicationContextAware {
public IFhirResourceDao getSubscriptionDao() { public IFhirResourceDao getSubscriptionDao() {
return getResourceDao(ResourceTypeEnum.SUBSCRIPTION.getCode()); return getResourceDao(ResourceTypeEnum.SUBSCRIPTION.getCode());
} }
public void setSupportedResourceTypes(String... theResourceTypes) {
setSupportedResourceTypes(toCollection(theResourceTypes));
}
private List<String> toCollection(String[] theResourceTypes) {
List<String> retVal = null;
if (theResourceTypes != null && theResourceTypes.length > 0) {
retVal = Arrays.asList(theResourceTypes);
}
return retVal;
}
} }

View File

@ -27,11 +27,6 @@ import org.hl7.fhir.instance.model.api.IBaseBundle;
public class FhirResourceDaoMessageHeaderDstu2 extends FhirResourceDaoDstu2<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> { public class FhirResourceDaoMessageHeaderDstu2 extends FhirResourceDaoDstu2<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> {
@Override
public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
public static IBaseBundle throwProcessMessageNotImplemented() { public static IBaseBundle throwProcessMessageNotImplemented() {
throw new NotImplementedOperationException("This operation is not yet implemented on this server"); throw new NotImplementedOperationException("This operation is not yet implemented on this server");
} }

View File

@ -60,6 +60,7 @@ import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.UrlUtil.UrlParts; import ca.uhn.fhir.util.UrlUtil.UrlParts;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -665,7 +666,11 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
} }
//@formatter:off @Override
public IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
/** /**
* Transaction Order, per the spec: * Transaction Order, per the spec:
@ -675,7 +680,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
* Process any PUT interactions * Process any PUT interactions
* Process any GET interactions * Process any GET interactions
*/ */
//@formatter:off
public class TransactionSorter implements Comparator<Entry> { public class TransactionSorter implements Comparator<Entry> {
@Override @Override

View File

@ -81,7 +81,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
super(); super();
} }
private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<? extends IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) { private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) {
if (theTerms == null) { if (theTerms == null) {
return; return;
} }
@ -171,13 +171,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
/* /*
* Handle _content parameter (resource body content) * Handle _content parameter (resource body content)
*/ */
List<List<? extends IQueryParameterType>> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT); List<List<IQueryParameterType>> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT);
addTextSearch(qb, bool, contentAndTerms, "myContentText", "myContentTextEdgeNGram", "myContentTextNGram"); addTextSearch(qb, bool, contentAndTerms, "myContentText", "myContentTextEdgeNGram", "myContentTextNGram");
/* /*
* Handle _text parameter (resource narrative content) * Handle _text parameter (resource narrative content)
*/ */
List<List<? extends IQueryParameterType>> textAndTerms = theParams.remove(Constants.PARAM_TEXT); List<List<IQueryParameterType>> textAndTerms = theParams.remove(Constants.PARAM_TEXT);
addTextSearch(qb, bool, textAndTerms, "myNarrativeText", "myNarrativeTextEdgeNGram", "myNarrativeTextNGram"); addTextSearch(qb, bool, textAndTerms, "myNarrativeText", "myNarrativeTextEdgeNGram", "myNarrativeTextNGram");
if (theReferencingPid != null) { if (theReferencingPid != null) {

View File

@ -1,7 +1,5 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
/* /*
@ -25,7 +23,5 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
*/ */
public interface IFhirResourceDaoMessageHeader<T extends IBaseResource> extends IFhirResourceDao<T> { public interface IFhirResourceDaoMessageHeader<T extends IBaseResource> extends IFhirResourceDao<T> {
// nothing right now
IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage);
} }

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome; import ca.uhn.fhir.jpa.util.ExpungeOutcome;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -60,6 +61,12 @@ public interface IFhirSystemDao<T, MT> extends IDao {
*/ */
MT metaGetOperation(RequestDetails theRequestDetails); MT metaGetOperation(RequestDetails theRequestDetails);
/**
* Implementations may implement this method to implement the $process-message
* operation
*/
IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage);
T transaction(RequestDetails theRequestDetails, T theResources); T transaction(RequestDetails theRequestDetails, T theResources);
} }

View File

@ -20,9 +20,10 @@ package ca.uhn.fhir.jpa.dao;
* #L% * #L%
*/ */
import java.io.Closeable;
import java.util.Iterator; import java.util.Iterator;
public interface IResultIterator extends Iterator<Long> { public interface IResultIterator extends Iterator<Long>, Closeable {
int getSkippedCount(); int getSkippedCount();

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao;
*/ */
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.DateRangeParam;
@ -34,7 +35,7 @@ import java.util.Set;
public interface ISearchBuilder { public interface ISearchBuilder {
IResultIterator createQuery(SearchParameterMap theParams, String theSearchUuid); IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime);
void setMaxResultsToFetch(Integer theMaxResultsToFetch); void setMaxResultsToFetch(Integer theMaxResultsToFetch);

View File

@ -29,15 +29,15 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.entity.ResourceSearchView; import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.util.StringNormalizer; import ca.uhn.fhir.jpa.model.util.StringNormalizer;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.term.VersionIndependentConcept; import ca.uhn.fhir.jpa.term.VersionIndependentConcept;
@ -51,15 +51,14 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@ -79,6 +78,7 @@ import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPre
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -108,7 +108,11 @@ public class SearchBuilder implements ISearchBuilder {
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
private static final int maxLoad = 800; /**
* @see #loadResourcesByPid(Collection, List, Set, boolean, EntityManager, FhirContext, IDao)
* for an explanation of why we use the constant 800
*/
private static final int MAXIMUM_PAGE_SIZE = 800;
private static Long NO_MORE = -1L; private static Long NO_MORE = -1L;
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest; private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
private static SearchParameterMap ourLastHandlerParamsForUnitTest; private static SearchParameterMap ourLastHandlerParamsForUnitTest;
@ -117,6 +121,8 @@ public class SearchBuilder implements ISearchBuilder {
private final boolean myDontUseHashesForSearch; private final boolean myDontUseHashesForSearch;
private final DaoConfig myDaoConfig; private final DaoConfig myDaoConfig;
@Autowired @Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IResourceTagDao myResourceTagDao; protected IResourceTagDao myResourceTagDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION) @PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager; protected EntityManager myEntityManager;
@ -140,8 +146,6 @@ public class SearchBuilder implements ISearchBuilder {
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired @Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
private List<Long> myAlsoIncludePids; private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder; private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao; private BaseHapiFhirDao<?> myCallingDao;
@ -156,6 +160,7 @@ public class SearchBuilder implements ISearchBuilder {
private int myFetchSize; private int myFetchSize;
private Integer myMaxResultsToFetch; private Integer myMaxResultsToFetch;
private Set<Long> myPidSet; private Set<Long> myPidSet;
private boolean myHaveIndexJoins = false;
/** /**
* Constructor * Constructor
@ -192,7 +197,7 @@ public class SearchBuilder implements ISearchBuilder {
private void addPredicateDate(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateDate(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
Join<ResourceTable, ResourceIndexedSearchParamDate> join = createOrReuseJoin(JoinEnum.DATE, theParamName); Join<ResourceTable, ResourceIndexedSearchParamDate> join = createJoin(JoinEnum.DATE, theParamName);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
Boolean missing = theList.get(0).getMissing(); Boolean missing = theList.get(0).getMissing();
@ -211,30 +216,29 @@ public class SearchBuilder implements ISearchBuilder {
} }
private void addPredicateHas(List<List<? extends IQueryParameterType>> theHasParameters) { private void addPredicateHas(List<List<IQueryParameterType>> theHasParameters) {
for (List<? extends IQueryParameterType> nextOrList : theHasParameters) { for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
StringBuilder valueBuilder = new StringBuilder();
String targetResourceType = null; String targetResourceType = null;
String owningParameter = null; String paramReference = null;
String parameterName = null; String parameterName = null;
String paramName = null;
List<QualifiedParamList> parameters = new ArrayList<>();
for (IQueryParameterType nextParam : nextOrList) { for (IQueryParameterType nextParam : nextOrList) {
HasParam next = (HasParam) nextParam; HasParam next = (HasParam) nextParam;
if (valueBuilder.length() > 0) {
valueBuilder.append(',');
}
valueBuilder.append(UrlUtil.escapeUrlParam(next.getValueAsQueryToken(myContext)));
targetResourceType = next.getTargetResourceType(); targetResourceType = next.getTargetResourceType();
owningParameter = next.getOwningFieldName(); paramReference = next.getReferenceFieldName();
parameterName = next.getParameterName(); parameterName = next.getParameterName();
paramName = parameterName.replaceAll("\\..*", "");
parameters.add(QualifiedParamList.singleton(paramName, next.getValueAsQueryToken(myContext)));
} }
if (valueBuilder.length() == 0) { if (paramName == null) {
continue; continue;
} }
String matchUrl = targetResourceType + '?' + UrlUtil.escapeUrlParam(parameterName) + '=' + valueBuilder.toString();
RuntimeResourceDefinition targetResourceDefinition; RuntimeResourceDefinition targetResourceDefinition;
try { try {
targetResourceDefinition = myContext.getResourceDefinition(targetResourceType); targetResourceDefinition = myContext.getResourceDefinition(targetResourceType);
@ -243,32 +247,37 @@ public class SearchBuilder implements ISearchBuilder {
} }
assert parameterName != null; assert parameterName != null;
String paramName = parameterName.replaceAll("\\..*", "");
RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName);
if (owningParameterDef == null) { if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName); throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName);
} }
owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, owningParameter); owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramReference);
if (owningParameterDef == null) { if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + owningParameter); throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + paramReference);
} }
Class<? extends IBaseResource> resourceType = targetResourceDefinition.getImplementingClass(); RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName);
Set<Long> match = myMatchResourceUrlService.processMatchUrl(matchUrl, resourceType);
if (match.isEmpty()) { IQueryParameterAnd<IQueryParameterOr<IQueryParameterType>> parsedParam = (IQueryParameterAnd<IQueryParameterOr<IQueryParameterType>>) ParameterUtil.parseQueryParams(myContext, paramDef, paramName, parameters);
// Pick a PID that can never match
match = Collections.singleton(-1L); ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
for (IQueryParameterOr<IQueryParameterType> next : parsedParam.getValuesAsQueryTokens()) {
orValues.addAll(next.getValuesAsQueryTokens());
} }
Subquery<Long> subQ = createLinkSubquery(true, parameterName, targetResourceType, orValues);
Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT); Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT);
Predicate pathPredicate = createResourceLinkPathPredicate(targetResourceType, paramReference, join);
Predicate predicate = join.get("mySourceResourcePid").in(match); Predicate pidPredicate = join.get("mySourceResourcePid").in(subQ);
myPredicates.add(predicate); Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
myPredicates.add(andPredicate);
} }
} }
private void addPredicateLanguage(List<List<? extends IQueryParameterType>> theList) { private void addPredicateLanguage(List<List<IQueryParameterType>> theList) {
for (List<? extends IQueryParameterType> nextList : theList) { for (List<? extends IQueryParameterType> nextList : theList) {
Set<String> values = new HashSet<>(); Set<String> values = new HashSet<>();
@ -280,7 +289,7 @@ public class SearchBuilder implements ISearchBuilder {
} }
values.add(nextValue); values.add(nextValue);
} else { } else {
throw new InternalErrorException("Lanugage parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName()); throw new InternalErrorException("Language parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName());
} }
} }
@ -296,7 +305,7 @@ public class SearchBuilder implements ISearchBuilder {
private void addPredicateNumber(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateNumber(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
Join<ResourceTable, ResourceIndexedSearchParamNumber> join = createOrReuseJoin(JoinEnum.NUMBER, theParamName); Join<ResourceTable, ResourceIndexedSearchParamNumber> join = createJoin(JoinEnum.NUMBER, theParamName);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
@ -356,7 +365,7 @@ public class SearchBuilder implements ISearchBuilder {
} }
private void addPredicateQuantity(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateQuantity(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
Join<ResourceTable, ResourceIndexedSearchParamQuantity> join = createOrReuseJoin(JoinEnum.QUANTITY, theParamName); Join<ResourceTable, ResourceIndexedSearchParamQuantity> join = createJoin(JoinEnum.QUANTITY, theParamName);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
@ -384,9 +393,10 @@ public class SearchBuilder implements ISearchBuilder {
return; return;
} }
Join<ResourceTable, ResourceLink> join = createOrReuseJoin(JoinEnum.REFERENCE, theParamName); Join<ResourceTable, ResourceLink> join = createJoin(JoinEnum.REFERENCE, theParamName);
List<Predicate> codePredicates = new ArrayList<>(); List<IIdType> targetIds = new ArrayList<>();
List<String> targetQualifiedUrls = new ArrayList<>();
for (int orIdx = 0; orIdx < theList.size(); orIdx++) { for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
IQueryParameterType nextOr = theList.get(orIdx); IQueryParameterType nextOr = theList.get(orIdx);
@ -395,39 +405,74 @@ public class SearchBuilder implements ISearchBuilder {
ReferenceParam ref = (ReferenceParam) nextOr; ReferenceParam ref = (ReferenceParam) nextOr;
if (isBlank(ref.getChain())) { if (isBlank(ref.getChain())) {
/*
* Handle non-chained search, e.g. Patient?organization=Organization/123
*/
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null); IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
if (dt.hasBaseUrl()) { if (dt.hasBaseUrl()) {
if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
dt = dt.toUnqualified(); dt = dt.toUnqualified();
targetIds.add(dt);
} else { } else {
ourLog.debug("Searching for resource link with target URL: {}", dt.getValue()); targetQualifiedUrls.add(dt.getValue());
Predicate eq = myBuilder.equal(join.get("myTargetResourceUrl"), dt.getValue());
codePredicates.add(eq);
continue;
} }
} else {
targetIds.add(dt);
} }
List<Long> targetPid; } else {
try {
targetPid = myIdHelperService.translateForcedIdToPids(dt);
} catch (ResourceNotFoundException e) {
// Use a PID that will never exist
targetPid = Collections.singletonList(-1L);
}
for (Long next : targetPid) {
ourLog.debug("Searching for resource link with target PID: {}", next);
/*
* Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
*/
addPredicateReferenceWithChain(theResourceName, theParamName, theList, join, new ArrayList<>(), ref);
return;
}
} else {
throw new IllegalArgumentException("Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
}
}
List<Predicate> codePredicates = new ArrayList<>();
// Resources by ID
List<Long> targetPids = myIdHelperService.translateForcedIdToPids(targetIds);
if (!targetPids.isEmpty()) {
ourLog.debug("Searching for resource link with target PIDs: {}", targetPids);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join); Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = myBuilder.equal(join.get("myTargetResourcePid"), next); Predicate pidPredicate = join.get("myTargetResourcePid").in(targetPids);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate)); codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
} }
} else { // Resources by fully qualified URL
if (!targetQualifiedUrls.isEmpty()) {
ourLog.debug("Searching for resource link with target URLs: {}", targetQualifiedUrls);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = join.get("myTargetResourceUrl").in(targetQualifiedUrls);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
}
if (codePredicates.size() > 0) {
myPredicates.add(myBuilder.or(toArray(codePredicates)));
} else {
// Add a predicate that will never match
Predicate pidPredicate = join.get("myTargetResourcePid").in(-1L);
myPredicates.clear();
myPredicates.add(pidPredicate);
}
}
private void addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, Join<ResourceTable, ResourceLink> theJoin, List<Predicate> theCodePredicates, ReferenceParam theRef) {
final List<Class<? extends IBaseResource>> resourceTypes; final List<Class<? extends IBaseResource>> resourceTypes;
String resourceId; String resourceId;
if (!ref.getValue().matches("[a-zA-Z]+/.*")) { if (!theRef.getValue().matches("[a-zA-Z]+/.*")) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
resourceTypes = new ArrayList<>(); resourceTypes = new ArrayList<>();
@ -485,16 +530,16 @@ public class SearchBuilder implements ISearchBuilder {
} }
} }
resourceId = ref.getValue(); resourceId = theRef.getValue();
} else { } else {
try { try {
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType()); RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theRef.getResourceType());
resourceTypes = new ArrayList<>(1); resourceTypes = new ArrayList<>(1);
resourceTypes.add(resDef.getImplementingClass()); resourceTypes.add(resDef.getImplementingClass());
resourceId = ref.getIdPart(); resourceId = theRef.getIdPart();
} catch (DataFormatException e) { } catch (DataFormatException e) {
throw new InvalidRequestException("Invalid resource type: " + ref.getResourceType()); throw new InvalidRequestException("Invalid resource type: " + theRef.getResourceType());
} }
} }
@ -502,7 +547,7 @@ public class SearchBuilder implements ISearchBuilder {
for (Class<? extends IBaseResource> nextType : resourceTypes) { for (Class<? extends IBaseResource> nextType : resourceTypes) {
String chain = ref.getChain(); String chain = theRef.getChain();
String remainingChain = null; String remainingChain = null;
int chainDotIndex = chain.indexOf('.'); int chainDotIndex = chain.indexOf('.');
if (chainDotIndex != -1) { if (chainDotIndex != -1) {
@ -548,12 +593,29 @@ public class SearchBuilder implements ISearchBuilder {
orValues.add(chainValue); orValues.add(chainValue);
} }
Subquery<Long> subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin);
Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ);
Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
theCodePredicates.add(andPredicate);
}
if (!foundChainMatch) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theRef.getChain()));
}
myPredicates.add(myBuilder.or(toArray(theCodePredicates)));
}
private Subquery<Long> createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List<IQueryParameterType> theOrValues) {
Subquery<Long> subQ = myResourceTableQuery.subquery(Long.class); Subquery<Long> subQ = myResourceTableQuery.subquery(Long.class);
Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class); Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class);
subQ.select(subQfrom.get("myId").as(Long.class)); subQ.select(subQfrom.get("myId").as(Long.class));
List<List<? extends IQueryParameterType>> andOrParams = new ArrayList<>(); List<List<IQueryParameterType>> andOrParams = new ArrayList<>();
andOrParams.add(orValues); andOrParams.add(theOrValues);
/* /*
* We're doing a chain call, so push the current query root * We're doing a chain call, so push the current query root
@ -568,11 +630,11 @@ public class SearchBuilder implements ISearchBuilder {
myIndexJoins = Maps.newHashMap(); myIndexJoins = Maps.newHashMap();
// Create the subquery predicates // Create the subquery predicates
myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), subResourceName)); myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), theSubResourceName));
myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted"))); myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted")));
if (foundChainMatch) { if (theFoundChainMatch) {
searchForIdsWithAndOr(subResourceName, chain, andOrParams); searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams);
subQ.where(toArray(myPredicates)); subQ.where(toArray(myPredicates));
} }
@ -582,29 +644,7 @@ public class SearchBuilder implements ISearchBuilder {
myResourceTableRoot = stackRoot; myResourceTableRoot = stackRoot;
myPredicates = stackPredicates; myPredicates = stackPredicates;
myIndexJoins = stackIndexJoins; myIndexJoins = stackIndexJoins;
return subQ;
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = join.get("myTargetResourcePid").in(subQ);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
}
if (!foundChainMatch) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain()));
}
myPredicates.add(myBuilder.or(toArray(codePredicates)));
return;
}
} else {
throw new IllegalArgumentException("Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
}
}
myPredicates.add(myBuilder.or(toArray(codePredicates)));
} }
private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class<? extends IBaseResource> nextType, String chain, boolean isMeta, String resourceId) { private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class<? extends IBaseResource> nextType, String chain, boolean isMeta, String resourceId) {
@ -629,7 +669,7 @@ public class SearchBuilder implements ISearchBuilder {
return chainValue; return chainValue;
} }
private void addPredicateResourceId(List<List<? extends IQueryParameterType>> theValues) { private void addPredicateResourceId(List<List<IQueryParameterType>> theValues) {
for (List<? extends IQueryParameterType> nextValue : theValues) { for (List<? extends IQueryParameterType> nextValue : theValues) {
Set<Long> orPids = new HashSet<>(); Set<Long> orPids = new HashSet<>();
for (IQueryParameterType next : nextValue) { for (IQueryParameterType next : nextValue) {
@ -638,22 +678,14 @@ public class SearchBuilder implements ISearchBuilder {
value = value.substring(1); value = value.substring(1);
} }
IdDt valueAsId = new IdDt(value); IdType valueAsId = new IdType(value);
if (isNotBlank(value)) { if (isNotBlank(value)) {
if (valueAsId.isIdPartValidLong()) {
orPids.add(valueAsId.getIdPartAsLong());
} else {
try { try {
BaseHasResource entity = myCallingDao.readEntity(valueAsId); Long pid = myIdHelperService.translateForcedIdToPid(myResourceName, valueAsId.getIdPart());
if (entity.getDeleted() == null) { orPids.add(pid);
orPids.add(entity.getId());
}
} catch (ResourceNotFoundException e) { } catch (ResourceNotFoundException e) {
/* // This is not an error in a search, it just results in no matchesFhirResourceDaoR4InterceptorTest
* This isn't an error, just means no result found ourLog.debug("Resource ID {} was requested but does not exist", valueAsId.getIdPart());
* that matches the ID the client provided
*/
}
} }
} }
} }
@ -672,7 +704,7 @@ public class SearchBuilder implements ISearchBuilder {
private void addPredicateString(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateString(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
Join<ResourceTable, ResourceIndexedSearchParamString> join = createOrReuseJoin(JoinEnum.STRING, theParamName); Join<ResourceTable, ResourceIndexedSearchParamString> join = createJoin(JoinEnum.STRING, theParamName);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
@ -689,7 +721,7 @@ public class SearchBuilder implements ISearchBuilder {
} }
private void addPredicateTag(List<List<? extends IQueryParameterType>> theList, String theParamName) { private void addPredicateTag(List<List<IQueryParameterType>> theList, String theParamName) {
TagTypeEnum tagType; TagTypeEnum tagType;
if (Constants.PARAM_TAG.equals(theParamName)) { if (Constants.PARAM_TAG.equals(theParamName)) {
tagType = TagTypeEnum.TAG; tagType = TagTypeEnum.TAG;
@ -820,13 +852,12 @@ public class SearchBuilder implements ISearchBuilder {
private void addPredicateToken(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateToken(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
Join<ResourceTable, ResourceIndexedSearchParamToken> join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); Join<ResourceTable, ResourceIndexedSearchParamToken> join = createJoin(JoinEnum.TOKEN, theParamName);
addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
return; return;
} }
List<Predicate> codePredicates = new ArrayList<>(); List<Predicate> codePredicates = new ArrayList<>();
Join<ResourceTable, ResourceIndexedSearchParamToken> join = null;
List<IQueryParameterType> tokens = new ArrayList<>(); List<IQueryParameterType> tokens = new ArrayList<>();
for (IQueryParameterType nextOr : theList) { for (IQueryParameterType nextOr : theList) {
@ -838,10 +869,6 @@ public class SearchBuilder implements ISearchBuilder {
} }
} }
if (join == null) {
join = createOrReuseJoin(JoinEnum.TOKEN, theParamName);
}
tokens.add(nextOr); tokens.add(nextOr);
} }
@ -849,6 +876,7 @@ public class SearchBuilder implements ISearchBuilder {
return; return;
} }
Join<ResourceTable, ResourceIndexedSearchParamToken> join = createJoin(JoinEnum.TOKEN, theParamName);
List<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join); List<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join);
codePredicates.addAll(singleCode); codePredicates.addAll(singleCode);
@ -858,7 +886,7 @@ public class SearchBuilder implements ISearchBuilder {
private void addPredicateUri(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { private void addPredicateUri(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
Join<ResourceTable, ResourceIndexedSearchParamUri> join = createOrReuseJoin(JoinEnum.URI, theParamName); Join<ResourceTable, ResourceIndexedSearchParamUri> join = createJoin(JoinEnum.URI, theParamName);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
@ -1005,9 +1033,7 @@ public class SearchBuilder implements ISearchBuilder {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> Join<ResourceTable, T> createOrReuseJoin(JoinEnum theType, String theSearchParameterName) { private <T> Join<ResourceTable, T> createJoin(JoinEnum theType, String theSearchParameterName) {
JoinKey key = new JoinKey(theSearchParameterName, theType);
return (Join<ResourceTable, T>) myIndexJoins.computeIfAbsent(key, k -> {
Join<ResourceTable, ResourceIndexedSearchParamDate> join = null; Join<ResourceTable, ResourceIndexedSearchParamDate> join = null;
switch (theType) { switch (theType) {
case DATE: case DATE:
@ -1032,8 +1058,12 @@ public class SearchBuilder implements ISearchBuilder {
join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT);
break; break;
} }
return join;
}); JoinKey key = new JoinKey(theSearchParameterName, theType);
myIndexJoins.put(key, join);
myHaveIndexJoins = true;
return (Join<ResourceTable, T>) join;
} }
private Predicate createPredicateDate(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamDate> theFrom) { private Predicate createPredicateDate(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamDate> theFrom) {
@ -1515,55 +1545,10 @@ public class SearchBuilder implements ISearchBuilder {
} }
@Override @Override
public IResultIterator createQuery(SearchParameterMap theParams, String theSearchUuid) { public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails) {
myParams = theParams; myParams = theParams;
myBuilder = myEntityManager.getCriteriaBuilder(); myBuilder = myEntityManager.getCriteriaBuilder();
mySearchUuid = theSearchUuid; mySearchUuid = theSearchRuntimeDetails.getSearchUuid();
/*
* Check if there is a unique key associated with the set
* of parameters passed in
*/
ourLog.debug("Checking for unique index for query: {}", theParams.toNormalizedQueryString(myContext));
if (myDaoConfig.isUniqueIndexesEnabled()) {
if (myParams.getIncludes().isEmpty()) {
if (myParams.getRevIncludes().isEmpty()) {
if (myParams.getEverythingMode() == null) {
if (myParams.isAllParametersHaveNoModifier()) {
Set<String> paramNames = theParams.keySet();
if (paramNames.isEmpty() == false) {
List<JpaRuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveUniqueSearchParams(myResourceName, paramNames);
if (searchParams.size() > 0) {
List<List<String>> params = new ArrayList<>();
for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamNameToValues : theParams.entrySet()) {
String nextParamName = nextParamNameToValues.getKey();
nextParamName = UrlUtil.escapeUrlParam(nextParamName);
for (List<? extends IQueryParameterType> nextAnd : nextParamNameToValues.getValue()) {
ArrayList<String> nextValueList = new ArrayList<>();
params.add(nextValueList);
for (IQueryParameterType nextOr : nextAnd) {
String nextOrValue = nextOr.getValueAsQueryToken(myContext);
nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
nextValueList.add(nextParamName + "=" + nextOrValue);
}
}
}
Set<String> uniqueQueryStrings = ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains(myResourceName, params);
if (ourTrackHandlersForUnitTest) {
ourLastHandlerParamsForUnitTest = theParams;
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX;
ourLastHandlerThreadForUnitTest = Thread.currentThread().getName();
}
return new UniqueIndexIterator(uniqueQueryStrings);
}
}
}
}
}
}
}
if (ourTrackHandlersForUnitTest) { if (ourTrackHandlersForUnitTest) {
ourLastHandlerParamsForUnitTest = theParams; ourLastHandlerParamsForUnitTest = theParams;
@ -1575,7 +1560,7 @@ public class SearchBuilder implements ISearchBuilder {
myPidSet = new HashSet<>(); myPidSet = new HashSet<>();
} }
return new QueryIterator(); return new QueryIterator(theSearchRuntimeDetails);
} }
private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount) { private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount) {
@ -1591,31 +1576,6 @@ public class SearchBuilder implements ISearchBuilder {
if (sort != null) { if (sort != null) {
assert !theCount; assert !theCount;
// outerQuery = myBuilder.createQuery(Long.class);
// Root<ResourceTable> outerQueryFrom = outerQuery.from(ResourceTable.class);
//
// List<Order> orders = Lists.newArrayList();
// List<Predicate> predicates = Lists.newArrayList();
//
// createSort(myBuilder, outerQueryFrom, sort, orders, predicates);
// if (orders.size() > 0) {
// outerQuery.orderBy(orders);
// }
//
// Subquery<Long> subQ = outerQuery.subquery(Long.class);
// Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class);
//
// myResourceTableQuery = subQ;
// myResourceTableRoot = subQfrom;
//
// Expression<Long> selectExpr = subQfrom.get("myId").as(Long.class);
// subQ.select(selectExpr);
//
// predicates.add(0, myBuilder.in(outerQueryFrom.get("myId").as(Long.class)).value(subQ));
//
// outerQuery.multiselect(outerQueryFrom.get("myId").as(Long.class));
// outerQuery.where(predicates.toArray(new Predicate[0]));
outerQuery = myBuilder.createQuery(Long.class); outerQuery = myBuilder.createQuery(Long.class);
myResourceTableQuery = outerQuery; myResourceTableQuery = outerQuery;
myResourceTableRoot = myResourceTableQuery.from(ResourceTable.class); myResourceTableRoot = myResourceTableQuery.from(ResourceTable.class);
@ -1633,7 +1593,6 @@ public class SearchBuilder implements ISearchBuilder {
outerQuery.orderBy(orders); outerQuery.orderBy(orders);
} }
} else { } else {
outerQuery = myBuilder.createQuery(Long.class); outerQuery = myBuilder.createQuery(Long.class);
@ -1702,7 +1661,7 @@ public class SearchBuilder implements ISearchBuilder {
* If we have any joins to index tables, we get this behaviour already guaranteed so we don't * If we have any joins to index tables, we get this behaviour already guaranteed so we don't
* need an explicit predicate for it. * need an explicit predicate for it.
*/ */
if (myIndexJoins.isEmpty()) { if (!myHaveIndexJoins) {
if (myParams.getEverythingMode() == null) { if (myParams.getEverythingMode() == null) {
myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), myResourceName)); myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), myResourceName));
} }
@ -1928,8 +1887,11 @@ public class SearchBuilder implements ISearchBuilder {
} }
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED // Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
HookParams params = new HookParams().add(IBaseResource.class, resource); HookParams params = new HookParams()
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params); .add(IBaseResource.class, resource)
.add(RequestDetails.class, null)
.add(ServletRequestDetails.class, null);
myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCE, params);
theResourceListToPopulate.set(index, resource); theResourceListToPopulate.set(index, resource);
} }
@ -1998,8 +1960,8 @@ public class SearchBuilder implements ISearchBuilder {
* but this should work too. Sigh. * but this should work too. Sigh.
*/ */
List<Long> pids = new ArrayList<>(theIncludePids); List<Long> pids = new ArrayList<>(theIncludePids);
for (int i = 0; i < pids.size(); i += maxLoad) { for (int i = 0; i < pids.size(); i += MAXIMUM_PAGE_SIZE) {
int to = i + maxLoad; int to = i + MAXIMUM_PAGE_SIZE;
to = Math.min(to, pids.size()); to = Math.min(to, pids.size());
List<Long> pidsSubList = pids.subList(i, to); List<Long> pidsSubList = pids.subList(i, to);
doLoadPids(theResourceListToPopulate, theIncludedPids, theForHistoryOperation, entityManager, context, theDao, position, pidsSubList); doLoadPids(theResourceListToPopulate, theIncludedPids, theForHistoryOperation, entityManager, context, theDao, position, pidsSubList);
@ -2046,7 +2008,7 @@ public class SearchBuilder implements ISearchBuilder {
if (matchAll) { if (matchAll) {
String sql; String sql;
sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) "; sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) ";
List<Collection<Long>> partitions = partition(nextRoundMatches, maxLoad); List<Collection<Long>> partitions = partition(nextRoundMatches, MAXIMUM_PAGE_SIZE);
for (Collection<Long> nextPartition : partitions) { for (Collection<Long> nextPartition : partitions) {
TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class); TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class);
q.setParameter("target_pids", nextPartition); q.setParameter("target_pids", nextPartition);
@ -2099,7 +2061,7 @@ public class SearchBuilder implements ISearchBuilder {
sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)";
} }
List<Collection<Long>> partitions = partition(nextRoundMatches, maxLoad); List<Collection<Long>> partitions = partition(nextRoundMatches, MAXIMUM_PAGE_SIZE);
for (Collection<Long> nextPartition : partitions) { for (Collection<Long> nextPartition : partitions) {
TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class); TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class);
q.setParameter("src_path", nextPath); q.setParameter("src_path", nextPath);
@ -2175,17 +2137,111 @@ public class SearchBuilder implements ISearchBuilder {
private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) { private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) {
myParams = theParams; myParams = theParams;
// Remove any empty parameters
theParams.clean(); theParams.clean();
for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
/*
* Check if there is a unique key associated with the set
* of parameters passed in
*/
boolean couldBeEligibleForCompositeUniqueSpProcessing =
myDaoConfig.isUniqueIndexesEnabled() &&
myParams.getEverythingMode() == null &&
myParams.isAllParametersHaveNoModifier();
if (couldBeEligibleForCompositeUniqueSpProcessing) {
// Since we're going to remove elements below
theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList));
List<JpaRuntimeSearchParam> activeUniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(myResourceName, theParams.keySet());
if (activeUniqueSearchParams.size() > 0) {
StringBuilder sb = new StringBuilder();
sb.append(myResourceName);
sb.append("?");
boolean first = true;
ArrayList<String> keys = new ArrayList<>(theParams.keySet());
Collections.sort(keys);
for (String nextParamName : keys) {
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
nextParamName = UrlUtil.escapeUrlParam(nextParamName);
if (nextValues.get(0).size() != 1) {
sb = null;
break;
}
// Reference params are only eligible for using a composite index if they
// are qualified
RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0);
if (isBlank(param.getResourceType())) {
sb = null;
break;
}
}
List<? extends IQueryParameterType> nextAnd = nextValues.remove(0);
IQueryParameterType nextOr = nextAnd.remove(0);
String nextOrValue = nextOr.getValueAsQueryToken(myContext);
nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
if (first) {
first = false;
} else {
sb.append('&');
}
sb.append(nextParamName).append('=').append(nextOrValue);
}
if (sb != null) {
String indexString = sb.toString();
ourLog.debug("Checking for unique index for query: {}", indexString);
if (ourTrackHandlersForUnitTest) {
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX;
}
addPredicateCompositeStringUnique(theParams, indexString);
}
}
}
// Handle each parameter
for (Entry<String, List<List<IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
String nextParamName = nextParamEntry.getKey(); String nextParamName = nextParamEntry.getKey();
List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue(); List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue();
searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams); searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams);
} }
} }
private void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) {
for (int i = 0; i < theListOfLists.size(); i++) {
List<T> oldSubList = theListOfLists.get(i);
if (!(oldSubList instanceof ArrayList)) {
List<T> newSubList = new ArrayList<>(oldSubList);
theListOfLists.set(i, newSubList);
}
}
}
private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexdString) {
myHaveIndexJoins = true;
Join<ResourceTable, ResourceIndexedCompositeStringUnique> join = myResourceTableRoot.join("myParamsCompositeStringUnique", JoinType.LEFT);
Predicate predicate = myBuilder.equal(join.get("myIndexString"), theIndexdString);
myPredicates.add(predicate);
// Remove any empty parameters remaining after this
theParams.clean();
}
private void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams) {
if (theAndOrParams.isEmpty()) { if (theAndOrParams.isEmpty()) {
return; return;
@ -2331,16 +2387,16 @@ public class SearchBuilder implements ISearchBuilder {
List<String> path = param.getPathsSplit(); List<String> path = param.getPathsSplit();
/* /*
* SearchParameters can declare paths on multiple resources * SearchParameters can declare paths on multiple resource
* types. Here we only want the ones that actually apply. * types. Here we only want the ones that actually apply.
*/ */
path = new ArrayList<>(path); path = new ArrayList<>(path);
for (int i = 0; i < path.size(); i++) { ListIterator<String> iter = path.listIterator();
String nextPath = trim(path.get(i)); while (iter.hasNext()) {
String nextPath = trim(iter.next());
if (!nextPath.contains(theResourceType + ".")) { if (!nextPath.contains(theResourceType + ".")) {
path.remove(i); iter.remove();
i--;
} }
} }
@ -2426,17 +2482,18 @@ public class SearchBuilder implements ISearchBuilder {
private final class QueryIterator extends BaseIterator<Long> implements IResultIterator { private final class QueryIterator extends BaseIterator<Long> implements IResultIterator {
private final SearchRuntimeDetails mySearchRuntimeDetails;
private boolean myFirst = true; private boolean myFirst = true;
private IncludesIterator myIncludesIterator; private IncludesIterator myIncludesIterator;
private Long myNext; private Long myNext;
private Iterator<Long> myPreResultsIterator; private Iterator<Long> myPreResultsIterator;
private Iterator<Long> myResultsIterator; private ScrollableResultsIterator<Long> myResultsIterator;
private SortSpec mySort; private SortSpec mySort;
private boolean myStillNeedToFetchIncludes; private boolean myStillNeedToFetchIncludes;
private StopWatch myStopwatch = null;
private int mySkipCount = 0; private int mySkipCount = 0;
private QueryIterator() { private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails) {
mySearchRuntimeDetails = theSearchRuntimeDetails;
mySort = myParams.getSort(); mySort = myParams.getSort();
// Includes are processed inline for $everything query // Includes are processed inline for $everything query
@ -2447,17 +2504,16 @@ public class SearchBuilder implements ISearchBuilder {
private void fetchNext() { private void fetchNext() {
if (myFirst) {
myStopwatch = new StopWatch();
}
// If we don't have a query yet, create one // If we don't have a query yet, create one
if (myResultsIterator == null) { if (myResultsIterator == null) {
if (myMaxResultsToFetch == null) { if (myMaxResultsToFetch == null) {
myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum(); myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum();
} }
final TypedQuery<Long> query = createQuery(mySort, myMaxResultsToFetch, false); final TypedQuery<Long> query = createQuery(mySort, myMaxResultsToFetch, false);
mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
Query<Long> hibernateQuery = (Query<Long>) query; Query<Long> hibernateQuery = (Query<Long>) query;
hibernateQuery.setFetchSize(myFetchSize); hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
@ -2478,8 +2534,6 @@ public class SearchBuilder implements ISearchBuilder {
if (myPidSet.add(next)) { if (myPidSet.add(next)) {
myNext = next; myNext = next;
break; break;
} else {
mySkipCount++;
} }
} }
} }
@ -2487,7 +2541,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myNext == null) { if (myNext == null) {
while (myResultsIterator.hasNext()) { while (myResultsIterator.hasNext()) {
Long next = myResultsIterator.next(); Long next = myResultsIterator.next();
if (next != null) if (next != null) {
if (myPidSet.add(next)) { if (myPidSet.add(next)) {
myNext = next; myNext = next;
break; break;
@ -2496,6 +2550,7 @@ public class SearchBuilder implements ISearchBuilder {
} }
} }
} }
}
if (myNext == null) { if (myNext == null) {
if (myStillNeedToFetchIncludes) { if (myStillNeedToFetchIncludes) {
@ -2509,8 +2564,6 @@ public class SearchBuilder implements ISearchBuilder {
if (myPidSet.add(next)) { if (myPidSet.add(next)) {
myNext = next; myNext = next;
break; break;
} else {
mySkipCount++;
} }
} }
if (myNext == null) { if (myNext == null) {
@ -2523,13 +2576,19 @@ public class SearchBuilder implements ISearchBuilder {
} // if we need to fetch the next result } // if we need to fetch the next result
mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
if (myFirst) { if (myFirst) {
ourLog.debug("Initial query result returned in {}ms for query {}", myStopwatch.getMillis(), mySearchUuid); HookParams params = new HookParams();
params.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
myInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params);
myFirst = false; myFirst = false;
} }
if (NO_MORE.equals(myNext)) { if (NO_MORE.equals(myNext)) {
ourLog.debug("Query found {} matches in {}ms for query {}", myPidSet.size(), myStopwatch.getMillis(), mySearchUuid); HookParams params = new HookParams();
params.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
myInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params);
} }
} }
@ -2555,48 +2614,15 @@ public class SearchBuilder implements ISearchBuilder {
public int getSkippedCount() { public int getSkippedCount() {
return mySkipCount; return mySkipCount;
} }
}
private class UniqueIndexIterator implements IResultIterator {
private final Set<String> myUniqueQueryStrings;
private Iterator<Long> myWrap = null;
UniqueIndexIterator(Set<String> theUniqueQueryStrings) {
myUniqueQueryStrings = theUniqueQueryStrings;
}
private void ensureHaveQuery() {
if (myWrap == null) {
ourLog.debug("Searching for unique index matches over {} candidate query strings", myUniqueQueryStrings.size());
StopWatch sw = new StopWatch();
Collection<Long> resourcePids = myResourceIndexedCompositeStringUniqueDao.findResourcePidsByQueryStrings(myUniqueQueryStrings);
ourLog.debug("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis());
myWrap = resourcePids.iterator();
}
}
@Override @Override
public boolean hasNext() { public void close() {
ensureHaveQuery(); if (myResultsIterator != null) {
return myWrap.hasNext(); myResultsIterator.close();
}
}
} }
@Override
public Long next() {
ensureHaveQuery();
return myWrap.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public int getSkippedCount() {
return 0;
}
}
private static class CountQueryIterator implements Iterator<Long> { private static class CountQueryIterator implements Iterator<Long> {
private final TypedQuery<Long> myQuery; private final TypedQuery<Long> myQuery;

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@ -64,7 +65,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType; import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.*;
@ -85,6 +88,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired @Autowired
private DaoRegistry myDaoRegistry; private DaoRegistry myDaoRegistry;
@Autowired(required = false)
private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect;
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) { if (theRequestDetails != null) {
@ -451,7 +456,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
} }
transactionStopWatch.endCurrentTask(); transactionStopWatch.endCurrentTask();
ourLog.info("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations()); ourLog.debug("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations());
return response; return response;
} }
@ -576,8 +581,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
*/ */
for (int i = 0; i < theEntries.size(); i++) { for (int i = 0; i < theEntries.size(); i++) {
if (i % 100 == 0) { if (i % 250 == 0) {
ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); ourLog.info("Processed {} non-GET entries out of {} in transaction", i, theEntries.size());
} }
BUNDLEENTRY nextReqEntry = theEntries.get(i); BUNDLEENTRY nextReqEntry = theEntries.get(i);
@ -749,7 +754,13 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
FhirTerser terser = myContext.newTerser(); FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
int i = 0;
for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
if (i++ % 250 == 0) {
ourLog.info("Have indexed {} entities out of {} in transaction", i, theIdToPersistedOutcome.values().size());
}
IBaseResource nextResource = nextOutcome.getResource(); IBaseResource nextResource = nextOutcome.getResource();
if (nextResource == null) { if (nextResource == null) {
continue; continue;
@ -803,7 +814,16 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
theTransactionStopWatch.endCurrentTask(); theTransactionStopWatch.endCurrentTask();
theTransactionStopWatch.startTask("Flush writes to database"); theTransactionStopWatch.startTask("Flush writes to database");
try {
flushJpaSession(); flushJpaSession();
} catch (PersistenceException e) {
if (myHapiFhirHibernateJpaDialect != null) {
List<String> types = theIdToPersistedOutcome.keySet().stream().filter(t -> t != null).map(t -> t.getResourceType()).collect(Collectors.toList());
String message = "Error flushing transaction with resource types: " + types;
throw myHapiFhirHibernateJpaDialect.translate(e, message);
}
throw e;
}
theTransactionStopWatch.endCurrentTask(); theTransactionStopWatch.endCurrentTask();
if (conditionalRequestUrls.size() > 0) { if (conditionalRequestUrls.size() > 0) {

View File

@ -31,15 +31,14 @@ import ca.uhn.fhir.jpa.model.entity.ForcedId;
public interface IForcedIdDao extends JpaRepository<ForcedId, Long> { public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f FROM ForcedId f WHERE myForcedId = :forced_id") // FIXME: JA We should log a performance warning if this is used since it's not indexed
public List<ForcedId> findByForcedId(@Param("forced_id") String theForcedId); @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)")
List<Long> findByForcedId(@Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id") @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN (:forced_id)")
public List<ForcedId> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); List<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid") @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid); ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid in (:pids)")
Collection<ForcedId> findByResourcePids(@Param("pids") Collection<Long> pids);
} }

View File

@ -1,10 +1,6 @@
package ca.uhn.fhir.jpa.dao.data; package ca.uhn.fhir.jpa.dao.data;
import java.util.Collection; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import java.util.Date;
import javax.persistence.TemporalType;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@ -13,7 +9,9 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.jpa.repository.Temporal;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import javax.persistence.TemporalType;
import java.util.Collection;
import java.util.Date;
/* /*
* #%L * #%L
@ -74,6 +72,13 @@ public interface IResourceHistoryTableDao extends JpaRepository<ResourceHistoryT
@Query("SELECT t.myId FROM ResourceHistoryTable t WHERE t.myResourceId = :resId AND t.myResourceVersion != :dontWantVersion") @Query("SELECT t.myId FROM ResourceHistoryTable t WHERE t.myResourceId = :resId AND t.myResourceVersion != :dontWantVersion")
Slice<Long> findForResourceId(Pageable thePage, @Param("resId") Long theId, @Param("dontWantVersion") Long theDontWantVersion); Slice<Long> findForResourceId(Pageable thePage, @Param("resId") Long theId, @Param("dontWantVersion") Long theDontWantVersion);
@Query("" +
"SELECT v.myId FROM ResourceHistoryTable v " +
"LEFT OUTER JOIN ResourceTable t ON (v.myResourceId = t.myId) " +
"WHERE v.myResourceVersion != t.myVersion AND " +
"t.myId = :resId")
Slice<Long> findIdsOfPreviousVersionsOfResourceId(Pageable thePage, @Param("resId") Long theResourceId);
@Query("" + @Query("" +
"SELECT v.myId FROM ResourceHistoryTable v " + "SELECT v.myId FROM ResourceHistoryTable v " +
"LEFT OUTER JOIN ResourceTable t ON (v.myResourceId = t.myId) " + "LEFT OUTER JOIN ResourceTable t ON (v.myResourceId = t.myId) " +

View File

@ -33,6 +33,6 @@ import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
public interface ISearchParamPresentDao extends JpaRepository<SearchParamPresent, Long> { public interface ISearchParamPresentDao extends JpaRepository<SearchParamPresent, Long> {
@Query("SELECT s FROM SearchParamPresent s WHERE s.myResource = :res") @Query("SELECT s FROM SearchParamPresent s WHERE s.myResource = :res")
public Collection<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource); Collection<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource);
} }

View File

@ -20,17 +20,9 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* #L% * #L%
*/ */
import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.dstu3.model.MessageHeader; import org.hl7.fhir.dstu3.model.MessageHeader;
import org.hl7.fhir.instance.model.api.IBaseBundle;
public class FhirResourceDaoMessageHeaderDstu3 extends FhirResourceDaoDstu3<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> { public class FhirResourceDaoMessageHeaderDstu3 extends FhirResourceDaoDstu3<MessageHeader> implements IFhirResourceDaoMessageHeader<MessageHeader> {
// nothing right now
@Override
public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
} }

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
*/ */
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2;
import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
@ -29,6 +30,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetai
import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Meta; import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -79,6 +81,11 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return retVal; return retVal;
} }
@Override
public IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) {
return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented();
}
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
@ -86,4 +93,5 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
} }

View File

@ -89,6 +89,14 @@ public class JpaValidationSupportDstu3 implements IJpaValidationSupportDstu3, Ap
return fetchResource(theCtx, CodeSystem.class, theSystem); return fetchResource(theCtx, CodeSystem.class, theSystem);
} }
@Override
public ValueSet fetchValueSet(FhirContext theCtx, String theSystem) {
if (isBlank(theSystem)) {
return null;
}
return fetchResource(theCtx, ValueSet.class, theSystem);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {

View File

@ -62,6 +62,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
Long valueOf; Long valueOf;
try { try {
valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, theId); valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, theId);
ourLog.trace("Translated {}/{} to resource PID {}", theType, theId, valueOf);
} catch (ResourceNotFoundException e) { } catch (ResourceNotFoundException e) {
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) { if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
return null; return null;
@ -86,7 +87,9 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
throw new InvalidRequestException("Resource " + resName + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit); throw new InvalidRequestException("Resource " + resName + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit);
} }
ourLog.trace("Resource PID {} is of type {}", valueOf, target.getResourceType());
if (!theTypeString.equals(target.getResourceType())) { if (!theTypeString.equals(target.getResourceType())) {
ourLog.error("Resource {} with PID {} was not of type {}", target.getIdDt().getValue(), target.getId(), theTypeString);
throw new UnprocessableEntityException( throw new UnprocessableEntityException(
"Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType()); "Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType());
} }

Some files were not shown because too many files have changed in this diff Show More