Add Consent Service (#1359)

* Initial consent svc

* Ongoing consent svc work

* Add docs

* Ongoing consent service work

* Work on consent service

* More work on consent svc

* License header updates

* Ongoing consent svc work

* Some test fixes

* Some test fixes

* More work on consent svc

* Tests working

* Test fix

* Propagate RequestDetails to everything in JPA server

* More interceptor tweaks

* Fix compile error

* One more tweak to captured SQL

* Ongoing interceptor tweaks

* Ongoing interceptor tweaks

* More interceptor tweaks

* Interceptor tweaks

* Tweaks to tests

* Fix tests

* Test fix

* Raise warnings when encoding extensions with missing values

* Consent service work

* More interceptor tweaks

* Consent interceptor tweaks

* Add logging to test
This commit is contained in:
James Agnew 2019-06-27 16:35:29 -04:00 committed by GitHub
parent 2bfbea4e6b
commit 10d969c514
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 7792 additions and 3312 deletions

View File

@ -0,0 +1,75 @@
package example;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Observation;
@SuppressWarnings("unused")
public class ConsentInterceptors {
//START SNIPPET: service
public class MyConsentService implements IConsentService {
/**
* Invoked once at the start of every request
*/
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// This means that all requests should flow through the consent service
// This has performance implications - If you know that some requests
// don't need consent checking it is a good idea to return
// ConsentOutcome.AUTHORIZED instead for those requests.
return ConsentOutcome.PROCEED;
}
/**
* Can a given resource be returned to the user?
*/
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
// In this basic example, we will filter out lab results so that they
// are never disclosed to the user. A real interceptor might do something
// more nuanced.
if (theResource instanceof Observation) {
Observation obs = (Observation)theResource;
if (obs.getCategoryFirstRep().hasCoding("http://hl7.org/fhir/codesystem-observation-category.html", "laboratory")) {
return ConsentOutcome.REJECT;
}
}
// Otherwise, allow the
return ConsentOutcome.PROCEED;
}
/**
* Modify resources that are being shown to the user
*/
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
// Don't return the subject for Observation resources
if (theResource instanceof Observation) {
Observation obs = (Observation)theResource;
obs.setSubject(null);
}
return ConsentOutcome.AUTHORIZED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// We could write an audit trail entry in here
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// We could write an audit trail entry in here
}
}
//END SNIPPET: service
}

View File

@ -39,4 +39,12 @@ public interface IInterceptorBroadcaster {
*/
Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams);
/**
* Does this broadcaster have any hooks for the given pointcut?
*
* @param thePointcut The poointcut
* @return Does this broadcaster have any hooks for the given pointcut?
* @since 4.0.0
*/
boolean hasHooks(Pointcut thePointcut);
}

View File

@ -225,11 +225,23 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.rest.api.RestOperationTypeEnum - The type of operation that the FHIR server has determined that the client is trying to invoke
* </li>
* <li>
* ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails - An object which will be populated with the details which were extracted from the raw request by the
* server, e.g. the FHIR operation type and the parsed resource body (if any).
* ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails - This parameter is provided for legacy reasons only and will be removed in the fututre. Do not use.
* </li>
* </ul>
* </p>
@ -242,6 +254,8 @@ public enum Pointcut {
* </p>
*/
SERVER_INCOMING_REQUEST_PRE_HANDLED(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.rest.api.RestOperationTypeEnum",
"ca.uhn.fhir.rest.server.interceptor.IServerInterceptor$ActionRequestDetails"
),
@ -605,7 +619,7 @@ public enum Pointcut {
SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED(void.class, "ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"),
/**
* Invoked when a resource may be returned to the user, whether as a part of a READ,
* Invoked when one or more resources may be returned to the user, whether as a part of a READ,
* a SEARCH, or even as the response to a CREATE/UPDATE, etc.
* <p>
* This hook is invoked when a resource has been loaded by the storage engine and
@ -621,7 +635,10 @@ public enum Pointcut {
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The resource being returned</li>
* <li>
* ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails - Contains details about the
* specific resources being returned.
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
@ -641,8 +658,131 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PREACCESS_RESOURCE(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
STORAGE_PREACCESS_RESOURCES(void.class,
"ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked when the storage engine is about to check for the existence of a pre-cached search
* whose results match the given search parameters.
* <p>
* Hooks may accept the following parameters:
* </p>
* <ul>
* <li>
* ca.uhn.fhir.jpa.searchparam.SearchParameterMap - Contains the details of the search being checked
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred. <b>Note that this parameter may be null in contexts where the request is not
* known, such as while processing searches</b>
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks may return <code>boolean</code>. If the hook method returns
* <code>false</code>, the server will not attempt to check for a cached
* search no matter what.
* </p>
*/
STORAGE_PRECHECK_FOR_CACHED_SEARCH(boolean.class,
"ca.uhn.fhir.jpa.searchparam.SearchParameterMap",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked when a search is starting, prior to creating a record for the search.
* <p>
* Hooks may accept the following parameters:
* </p>
* <ul>
* <li>
* ca.uhn.fhir.rest.server.util.ICachedSearchDetails - Contains the details of the search that
* is being created and initialized
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred. <b>Note that this parameter may be null in contexts where the request is not
* known, such as while processing searches</b>
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESEARCH_REGISTERED(void.class,
"ca.uhn.fhir.rest.server.util.ICachedSearchDetails",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked when one or more resources may be returned to the user, whether as a part of a READ,
* a SEARCH, or even as the response to a CREATE/UPDATE, etc.
* <p>
* This hook is invoked when a resource has been loaded by the storage engine and
* is being returned to the HTTP stack for response.
* This is not a guarantee that the
* client will ultimately see it, since filters/headers/etc may affect what
* is returned but if a resource is loaded it is likely to be used.
* Note also that caching may affect whether this pointcut is invoked.
* </p>
* <p>
* Hooks will have access to the contents of the resource being returned
* and may choose to make modifications. These changes will be reflected in
* returned resource but have no effect on storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.IPreResourceShowDetails - Contains the resources that
* will be shown to the user. This object may be manipulated in order to modify
* the actual resources being shown to the user (e.g. for masking)
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred. <b>Note that this parameter may be null in contexts where the request is not
* known, such as while processing searches</b>
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESHOW_RESOURCES(void.class,
"ca.uhn.fhir.rest.api.server.IPreResourceShowDetails",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
@ -680,8 +820,79 @@ public enum Pointcut {
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
),
/**
* Invoked before a resource will be updated, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being updated
* (both the previous and new contents) and may choose to make modifications
* to the new contents of the resource. These changes will be reflected in
* permanent storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The previous contents of the resource being updated</li>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The new contents of the resource being updated</li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESTORAGE_RESOURCE_UPDATED(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked before a resource will be created, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being created
* and may choose to make modifications to it. These changes will be
* reflected in permanent storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The resource being deleted</li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESTORAGE_RESOURCE_DELETED(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked before a resource will be created, immediately before the transaction
* is committed (after all validation and other business rules have successfully
@ -717,41 +928,7 @@ public enum Pointcut {
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked before a resource will be created
* <p>
* Hooks will have access to the contents of the resource being deleted
* but should not make any changes as storage has already occurred
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The resource being deleted</li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRECOMMIT_RESOURCE_DELETED(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
),
/**
* Invoked before a resource will be updated, immediately before the transaction
@ -790,53 +967,12 @@ public enum Pointcut {
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
),
/**
* Invoked before a resource will be updated, immediately before the resource
* is persisted to the database.
* Invoked before a resource will be created
* <p>
* Hooks will have access to the contents of the resource being updated
* (both the previous and new contents) and may choose to make modifications
* to the new contents of the resource. These changes will be reflected in
* permanent storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The previous contents of the resource being updated</li>
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The new contents of the resource being updated</li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESTORAGE_RESOURCE_UPDATED(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Invoked before a resource will be created, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being created
* and may choose to make modifications to it. These changes will be
* reflected in permanent storage.
* Hooks will have access to the contents of the resource being deleted
* but should not make any changes as storage has already occurred
* </p>
* Hooks may accept the following parameters:
* <ul>
@ -859,11 +995,12 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
STORAGE_PRESTORAGE_RESOURCE_DELETED(void.class,
STORAGE_PRECOMMIT_RESOURCE_DELETED(void.class,
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
),
/**
* Invoked when a resource delete operation is about to fail due to referential integrity conflicts.
@ -873,6 +1010,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>ca.uhn.fhir.jpa.delete.DeleteConflictList - The list of delete conflicts</li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return <code>boolean</code>. If the method returns <code>true</code> then the caller
@ -883,20 +1033,74 @@ public enum Pointcut {
* {@value ca.uhn.fhir.jpa.delete.DeleteConflictService#MAX_RETRY_COUNT} conflicts to the hook.
* </p>
*/
STORAGE_PRESTORAGE_DELETE_CONFLICTS(boolean.class, "ca.uhn.fhir.jpa.delete.DeleteConflictList"),
STORAGE_PRESTORAGE_DELETE_CONFLICTS(boolean.class,
"ca.uhn.fhir.jpa.delete.DeleteConflictList",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost.
* <p>
* This hook is invoked when any informational messages generated by the
* SearchCoordinator are created. It is typically used to provide logging
* or capture details related to a specific request.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.StorageProcessingMessage - Contains the message
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_INFO(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.StorageProcessingMessage"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost.
* <p>
* This hook is invoked when any informational or warning messages generated by the
* This hook is invoked when any warning messages generated by the
* SearchCoordinator are created. It is typically used to provide logging
* or capture details related to a specific request.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.StorageProcessingMessage - Contains the message
* </li>
* </ul>
@ -905,8 +1109,10 @@ public enum Pointcut {
* </p>
*/
JPA_PERFTRACE_WARNING(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.StorageProcessingMessage"
),
),
/**
* Note that this is a performance tracing hook. Use with caution in production
@ -919,6 +1125,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails - Contains details about the search being
* performed. Hooks should not modify this object.
* </li>
@ -927,7 +1146,11 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED(void.class, "ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"),
JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
@ -942,6 +1165,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails - Contains details about the search being
* performed. Hooks should not modify this object.
* </li>
@ -950,7 +1186,11 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_SELECT_COMPLETE(void.class, "ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"),
JPA_PERFTRACE_SEARCH_SELECT_COMPLETE(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
@ -962,6 +1202,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails - Contains details about the search being
* performed. Hooks should not modify this object.
* </li>
@ -970,7 +1223,11 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_FAILED(void.class, "ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"),
JPA_PERFTRACE_SEARCH_FAILED(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
@ -984,6 +1241,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails - Contains details about the search being
* performed. Hooks should not modify this object.
* </li>
@ -992,7 +1262,11 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_PASS_COMPLETE(void.class, "ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"),
JPA_PERFTRACE_SEARCH_PASS_COMPLETE(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
@ -1005,6 +1279,19 @@ public enum Pointcut {
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails - Contains details about the search being
* performed. Hooks should not modify this object.
* </li>
@ -1013,7 +1300,48 @@ public enum Pointcut {
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_COMPLETE(void.class, "ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"),
JPA_PERFTRACE_SEARCH_COMPLETE(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails"
),
/**
* Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost.
* <p>
* This hook is invoked when a query has executed, and includes the raw SQL
* statements that were executed against the database.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>
* ca.uhn.fhir.jpa.util.SqlQueryList - Contains details about the raw SQL queries.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_RAW_SQL(void.class,
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.util.SqlQueryList"
),
/**
* This pointcut is used only for unit tests. Do not use in production code as it may be changed or

View File

@ -234,10 +234,21 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
@Override
public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams);
assert thePointcut.getReturnType() != void.class && thePointcut.getReturnType() != boolean.class;
assert thePointcut.getReturnType() != void.class;
Object retVal = doCallHooks(thePointcut, theParams, null);
return retVal;
return doCallHooks(thePointcut, theParams, null);
}
@Override
public boolean hasHooks(Pointcut thePointcut) {
return myGlobalInvokers.containsKey(thePointcut)
|| myAnonymousInvokers.containsKey(thePointcut)
|| hasThreadLocalHooks(thePointcut);
}
private boolean hasThreadLocalHooks(Pointcut thePointcut) {
ListMultimap<Pointcut, BaseInvoker> hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null;
return hooks != null && hooks.containsKey(thePointcut);
}
@Override

View File

@ -902,9 +902,7 @@ public abstract class BaseParser implements IParser {
}
String currentResourceName = theEncodeContext.getResourcePath().get(theEncodeContext.getResourcePath().size() - 1).getName();
if (myEncodeElementsAppliesToResourceTypes == null || myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) {
return true;
}
return myEncodeElementsAppliesToResourceTypes == null || myEncodeElementsAppliesToResourceTypes.contains(currentResourceName);
}
return false;
@ -944,9 +942,7 @@ public abstract class BaseParser implements IParser {
String resourceName = myContext.getResourceDefinition(theResource).getName();
if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) {
return false;
} else if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("*." + thePath))) {
return false;
}
} else return myDontEncodeElements.stream().noneMatch(t -> t.equalsPath("*." + thePath));
}
return true;
}
@ -1187,7 +1183,7 @@ public abstract class BaseParser implements IParser {
@Override
public String toString() {
return myPath.toString();
return myPath.stream().map(t->t.toString()).collect(Collectors.joining("."));
}
protected List<EncodeContextPathElement> getPath() {
@ -1328,10 +1324,7 @@ public abstract class BaseParser implements IParser {
return true;
}
}
if (myName.equals("*")) {
return true;
}
return false;
return myName.equals("*");
}
@Override

View File

@ -68,4 +68,8 @@ public class ErrorHandlerAdapter implements IParserErrorHandler {
// NOP
}
@Override
public void extensionContainsValueAndNestedExtensions(IParseLocation theLoc) {
// NOP
}
}

View File

@ -31,9 +31,8 @@ public interface IParserErrorHandler {
/**
* Invoked when a contained resource is parsed that has no ID specified (and is therefore invalid)
*
* @param theLocation
* The location in the document. WILL ALWAYS BE NULL currently, as this is not yet implemented, but this parameter is included so that locations can be added in the future without
* changing the API.
* @param theLocation The location in the document. WILL ALWAYS BE NULL currently, as this is not yet implemented, but this parameter is included so that locations can be added in the future without
* changing the API.
* @since 2.0
*/
void containedResourceWithNoId(IParseLocation theLocation);
@ -41,15 +40,13 @@ public interface IParserErrorHandler {
/**
* Invoked if the wrong type of element is found while parsing JSON. For example if a given element is
* expected to be a JSON Object and is a JSON array
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName
* The name of the element that was found.
* @param theExpectedValueType The datatype that was expected at this location
* @param theExpectedScalarType If theExpectedValueType is {@link ValueType#SCALAR}, this is the specific scalar type expected. Otherwise this parameter will be null.
* @param theFoundValueType The datatype that was found at this location
* @param theFoundScalarType If theFoundValueType is {@link ValueType#SCALAR}, this is the specific scalar type found. Otherwise this parameter will be null.
*
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName The name of the element that was found.
* @param theExpectedValueType The datatype that was expected at this location
* @param theExpectedScalarType If theExpectedValueType is {@link ValueType#SCALAR}, this is the specific scalar type expected. Otherwise this parameter will be null.
* @param theFoundValueType The datatype that was found at this location
* @param theFoundScalarType If theFoundValueType is {@link ValueType#SCALAR}, this is the specific scalar type found. Otherwise this parameter will be null.
* @since 2.2
*/
void incorrectJsonType(IParseLocation theLocation, String theElementName, ValueType theExpectedValueType, ScalarType theExpectedScalarType, ValueType theFoundValueType, ScalarType theFoundScalarType);
@ -57,10 +54,9 @@ public interface IParserErrorHandler {
/**
* The parser detected an attribute value that was invalid (such as: empty "" values are not permitted)
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theValue The actual value
* @param theError A description of why the value was invalid
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theValue The actual value
* @param theError A description of why the value was invalid
* @since 2.2
*/
void invalidValue(IParseLocation theLocation, String theValue, String theError);
@ -68,8 +64,7 @@ public interface IParserErrorHandler {
/**
* Resource was missing a required element
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName The missing element name
* @since 2.1
*/
@ -79,10 +74,8 @@ public interface IParserErrorHandler {
* Invoked when an element repetition (e.g. a second repetition of something) is found for a field
* which is non-repeating.
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName
* The name of the element that was found.
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName The name of the element that was found.
* @since 1.2
*/
void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName);
@ -90,20 +83,16 @@ public interface IParserErrorHandler {
/**
* Invoked when an unknown element is found in the document.
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theAttributeName
* The name of the attribute that was found.
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theAttributeName The name of the attribute that was found.
*/
void unknownAttribute(IParseLocation theLocation, String theAttributeName);
/**
* Invoked when an unknown element is found in the document.
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName
* The name of the element that was found.
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theElementName The name of the element that was found.
*/
void unknownElement(IParseLocation theLocation, String theElementName);
@ -111,13 +100,19 @@ public interface IParserErrorHandler {
* Resource contained a reference that could not be resolved and needs to be resolvable (e.g. because
* it is a local reference to an unknown contained resource)
*
* @param theLocation
* The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theLocation The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
* @param theReference The actual invalid reference (e.g. "#3")
* @since 2.0
*/
void unknownReference(IParseLocation theLocation, String theReference);
/**
* An extension contains both a value and at least one nested extension
*
* @param theLoc The location in the document. Note that this may be <code>null</code> as the ParseLocation feature is experimental. Use with caution, as the API may change.
*/
void extensionContainsValueAndNestedExtensions(IParseLocation theLocation);
/**
* For now this is an empty interface. Error handling methods include a parameter of this
* type which will currently always be set to null. This interface is included here so that
@ -127,6 +122,7 @@ public interface IParserErrorHandler {
/**
* Returns the name of the parent element (the element containing the element currently being parsed)
*
* @since 2.1
*/
String getParentElementName();

View File

@ -1434,42 +1434,65 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
JsonParser.write(theEventWriter, "id", getCompositeElementId(ext));
}
if (isBlank(extensionUrl)) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString());
getErrorHandler().missingRequiredElement(loc, "url");
}
JsonParser.write(theEventWriter, "url", extensionUrl);
boolean noValue = value == null || value.isEmpty();
if (noValue && ext.getExtension().isEmpty()) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString());
getErrorHandler().missingRequiredElement(loc, "value");
ourLog.debug("Extension with URL[{}] has no value", extensionUrl);
} else if (noValue) {
if (myModifier) {
beginArray(theEventWriter, "modifierExtension");
} else {
beginArray(theEventWriter, "extension");
}
for (Object next : ext.getExtension()) {
writeUndeclaredExtension(theResDef, theResource, theEventWriter, (IBaseExtension<?, ?>) next, theEncodeContext);
}
theEventWriter.endArray();
} else {
/*
* Pre-process value - This is called in case the value is a reference
* since we might modify the text
*/
value = preProcessValues(myDef, theResource, Collections.singletonList(value), myChildElem, theEncodeContext).get(0);
if (!noValue && !ext.getExtension().isEmpty()) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString());
getErrorHandler().extensionContainsValueAndNestedExtensions(loc);
}
// Write child extensions
if (!ext.getExtension().isEmpty()) {
if (myModifier) {
beginArray(theEventWriter, "modifierExtension");
} else {
beginArray(theEventWriter, "extension");
}
for (Object next : ext.getExtension()) {
writeUndeclaredExtension(theResDef, theResource, theEventWriter, (IBaseExtension<?, ?>) next, theEncodeContext);
}
theEventWriter.endArray();
RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
String childName = extDef.getChildNameByDatatype(value.getClass());
if (childName == null) {
childName = "value" + WordUtils.capitalize(myContext.getElementDefinition(value.getClass()).getName());
}
BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
if (childDef == null) {
throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName());
// Write value
if (!noValue) {
/*
* Pre-process value - This is called in case the value is a reference
* since we might modify the text
*/
value = preProcessValues(myDef, theResource, Collections.singletonList(value), myChildElem, theEncodeContext).get(0);
RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
String childName = extDef.getChildNameByDatatype(value.getClass());
if (childName == null) {
childName = "value" + WordUtils.capitalize(myContext.getElementDefinition(value.getClass()).getName());
}
BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
if (childDef == null) {
throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName());
}
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, false, myParent,false, theEncodeContext);
managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName, theEncodeContext);
}
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, false, myParent,false, theEncodeContext);
managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName, theEncodeContext);
}
// theEventWriter.name(myUndeclaredExtension.get);

View File

@ -158,6 +158,13 @@ public class LenientErrorHandler implements IParserErrorHandler {
}
}
@Override
public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
if (myLogErrors) {
ourLog.warn("Extension contains both a value and nested extensions: {}", theLocation);
}
}
public static String createIncorrectJsonTypeMessage(String theElementName, ValueType theExpected, ScalarType theExpectedScalarType, ValueType theFound, ScalarType theFoundScalarType) {
StringBuilder b = new StringBuilder();
b.append("Found incorrect type for element ");

View File

@ -22,6 +22,8 @@ package ca.uhn.fhir.parser;
import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation;
import static org.apache.commons.lang3.StringUtils.defaultString;
class ParseLocation implements IParseLocation {
private String myParentElementName;
@ -29,18 +31,29 @@ class ParseLocation implements IParseLocation {
/**
* Constructor
*/
public ParseLocation() {
ParseLocation() {
super();
}
/**
* Constructor
*/
ParseLocation(String theParentElementName) {
setParentElementName(theParentElementName);
}
@Override
public String getParentElementName() {
return myParentElementName;
}
public ParseLocation setParentElementName(String theParentElementName) {
ParseLocation setParentElementName(String theParentElementName) {
myParentElementName = theParentElementName;
return this;
}
@Override
public String toString() {
return defaultString(myParentElementName);
}
}

View File

@ -83,4 +83,9 @@ public class StrictErrorHandler implements IParserErrorHandler {
throw new DataFormatException("Resource has invalid reference: " + theReference);
}
@Override
public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
throw new DataFormatException("Extension contains both a value and nested extensions: " + theLocation);
}
}

View File

@ -406,7 +406,20 @@ public class XmlParser extends BaseParser /* implements IParser */ {
BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef();
String extensionUrl = getExtensionUrl(nextChild.getExtensionUrl());
if (extensionUrl != null && childName.equals("extension") == false) {
boolean isExtension = childName.equals("extension") || childName.equals("modifierExtension");
if (isExtension && nextValue instanceof IBaseExtension) {
IBaseExtension<?, ?> ext = (IBaseExtension<?, ?>) nextValue;
if (isBlank(ext.getUrl())) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString() + "." + childName);
getErrorHandler().missingRequiredElement(loc, "url");
}
if (ext.getValue() != null && ext.getExtension().size() > 0) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString() + "." + childName);
getErrorHandler().extensionContainsValueAndNestedExtensions(loc);
}
}
if (extensionUrl != null && isExtension == false) {
encodeExtension(theResource, theEventWriter, theContainedResource, nextChildElem, nextChild, nextValue, childName, extensionUrl, childDef, theEncodeContext);
} else if (nextChild instanceof RuntimeChildExtension) {
IBaseExtension<?, ?> extension = (IBaseExtension<?, ?>) nextValue;
@ -441,6 +454,11 @@ public class XmlParser extends BaseParser /* implements IParser */ {
theEventWriter.writeAttribute("id", elementId);
}
if (isBlank(extensionUrl)) {
ParseLocation loc = new ParseLocation(theEncodeContext.toString());
getErrorHandler().missingRequiredElement(loc, "url");
}
theEventWriter.writeAttribute("url", extensionUrl);
encodeChildElementToStreamWriter(theResource, theEventWriter, nextChild, nextValue, childName, childDef, null, theContainedResource, nextChildElem, theEncodeContext);
theEventWriter.writeEndElement();

View File

@ -21,10 +21,13 @@ package ca.uhn.fhir.rest.api;
*/
import ca.uhn.fhir.util.CoverageIgnore;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -35,6 +38,7 @@ public class MethodOutcome {
private IBaseOperationOutcome myOperationOutcome;
private IBaseResource myResource;
private Map<String, List<String>> myResponseHeaders;
private Collection<Runnable> myResourceViewCallbacks;
/**
* Constructor
@ -174,6 +178,7 @@ public class MethodOutcome {
* </p>
*
* @return Returns a reference to <code>this</code> for easy method chaining
* @see #registerResourceViewCallback(Runnable) to register a callback that should be invoked by the framework before the resource is shown/returned to a client
*/
public MethodOutcome setResource(IBaseResource theResource) {
myResource = theResource;
@ -194,6 +199,35 @@ public class MethodOutcome {
myResponseHeaders = theResponseHeaders;
}
/**
* Registers a callback to be invoked before the resource in this object gets
* returned to the client. Note that this is an experimental API and may change.
*
* @param theCallback The callback
* @since 4.0.0
*/
public void registerResourceViewCallback(Runnable theCallback) {
Validate.notNull(theCallback, "theCallback must not be null");
if (myResourceViewCallbacks == null) {
myResourceViewCallbacks = new ArrayList<>(2);
}
myResourceViewCallbacks.add(theCallback);
}
/**
* Fires callbacks registered to {@link #registerResourceViewCallback(Runnable)} and then
* clears the list of registered callbacks.
*
* @since 4.0.0
*/
public void fireResourceViewCallbacks() {
if (myResourceViewCallbacks != null) {
myResourceViewCallbacks.forEach(t -> t.run());
myResourceViewCallbacks.clear();
}
}
public void setCreatedUsingStatusCode(int theResponseStatusCode) {
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
setCreated(true);

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* This object is an abstraction for a server response that is going to
* return one or more resources to the user. This can be used by interceptors
* to make decisions about whether a resource should be visible or not
* to the user making the request.
*/
public interface IPreResourceAccessDetails {
int size();
IBaseResource getResource(int theIndex);
void setDontReturnResourceAtIndex(int theIndex);
}

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* This interface is a parameter type for the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCE}
* hook.
*/
public interface IPreResourceShowDetails {
/**
* @return Returns the number of resources being shown
*/
int size();
/**
* @return Returns the resource at the given index. If you wish to make modifications
* to any resources
*/
IBaseResource getResource(int theIndex);
/**
* Replace the resource being returned at index
*
* @param theIndex The resource index
* @param theResource The resource at index
*/
void setResource(int theIndex, IBaseResource theResource);
/**
* Indicates that data is being masked from within the resource at the given index.
* This generally flags to the rest of the stack that the resource should include
* a SUBSET tag as an indication to consumers that some data has been removed.
*
* @param theIndex The resource index
*/
void markResourceAtIndexAsSubset(int theIndex);
}

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Collections;
import java.util.List;
public class SimplePreResourceAccessDetails implements IPreResourceAccessDetails {
private final List<IBaseResource> myResources;
private final boolean[] myBlocked;
public SimplePreResourceAccessDetails(IBaseResource theResource) {
this(Collections.singletonList(theResource));
}
public <T extends IBaseResource> SimplePreResourceAccessDetails(List<T> theResources) {
//noinspection unchecked
myResources = (List<IBaseResource>) theResources;
myBlocked = new boolean[myResources.size()];
}
@Override
public int size() {
return myResources.size();
}
@Override
public IBaseResource getResource(int theIndex) {
return myResources.get(theIndex);
}
@Override
public void setDontReturnResourceAtIndex(int theIndex) {
myBlocked[theIndex] = true;
}
public boolean isDontReturnResourceAtIndex(int theIndex) {
return myBlocked[theIndex];
}
/**
* Remove any blocked resources from the list that was passed into the constructor
*/
public void applyFilterToList() {
for (int i = size() - 1; i >= 0; i--) {
if (isDontReturnResourceAtIndex(i)) {
myResources.remove(i);
}
}
}
}

View File

@ -0,0 +1,47 @@
package ca.uhn.fhir.rest.api.server;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
public class SimplePreResourceShowDetails implements IPreResourceShowDetails {
private final List<IBaseResource> myResources;
private final boolean[] mySubSets;
public SimplePreResourceShowDetails(IBaseResource theResource) {
this(Lists.newArrayList(theResource));
}
public <T extends IBaseResource> SimplePreResourceShowDetails(List<T> theResources) {
//noinspection unchecked
myResources = (List<IBaseResource>) theResources;
mySubSets = new boolean[myResources.size()];
}
@Override
public int size() {
return myResources.size();
}
@Override
public IBaseResource getResource(int theIndex) {
return myResources.get(theIndex);
}
@Override
public void setResource(int theIndex, IBaseResource theResource) {
Validate.isTrue(theIndex >= 0, "Invalid index %d - theIndex must not be < 0", theIndex);
Validate.isTrue(theIndex < myResources.size(), "Invalid index {} - theIndex must be < %d", theIndex, myResources.size());
myResources.set(theIndex, theResource);
}
@Override
public void markResourceAtIndexAsSubset(int theIndex) {
Validate.isTrue(theIndex >= 0, "Invalid index %d - theIndex must not be < 0", theIndex);
Validate.isTrue(theIndex < myResources.size(), "Invalid index {} - theIndex must be < %d", theIndex, myResources.size());
mySubSets[theIndex] = true;
}
}

View File

@ -27,7 +27,16 @@ public interface IHistoryUntyped {
/**
* Request that the method return a Bundle resource (such as <code>ca.uhn.fhir.model.dstu2.resource.Bundle</code>).
* Use this method if you are accessing a DSTU2+ server.
* @deprecated Use {@link #returnBundle(Class)} instead, which has the exact same functionality. This was deprecated in HAPI FHIR 4.0.0 in order to be consistent with the similar method on the search operation.
*/
@Deprecated
<T extends IBaseBundle> IHistoryTyped<T> andReturnBundle(Class<T> theType);
/**
* Request that the method return a Bundle resource (such as <code>ca.uhn.fhir.model.dstu2.resource.Bundle</code>).
* Use this method if you are accessing a DSTU2+ server.
* @since 4.0.0
*/
<T extends IBaseBundle> IHistoryTyped<T> returnBundle(Class<T> theType);
}

View File

@ -1,5 +1,20 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
@ -22,21 +37,36 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* #L%
*/
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
/**
* Fetch resources from a bundle
*/
public class BundleUtil {
public static class BundleEntryParts {
private final RequestTypeEnum myRequestType;
private final IBaseResource myResource;
private final String myUrl;
BundleEntryParts(RequestTypeEnum theRequestType, String theUrl, IBaseResource theResource) {
super();
myRequestType = theRequestType;
myUrl = theUrl;
myResource = theResource;
}
public RequestTypeEnum getRequestType() {
return myRequestType;
}
public IBaseResource getResource() {
return myResource;
}
public String getUrl() {
return myUrl;
}
}
/**
* @return Returns <code>null</code> if the link isn't found or has no value
*/
@ -137,6 +167,14 @@ public class BundleUtil {
return null;
}
public static void setTotal(FhirContext theContext, IBaseBundle theBundle, Integer theTotal) {
RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
BaseRuntimeChildDefinition entryChild = def.getChildByName("total");
IPrimitiveType<Integer> value = (IPrimitiveType<Integer>) entryChild.getChildByName("total").newInstance();
value.setValue(theTotal);
entryChild.getMutator().setValue(theBundle, value);
}
/**
* Extract all of the resources from a given bundle
*/
@ -216,29 +254,4 @@ public class BundleUtil {
}
return retVal;
}
public static class BundleEntryParts {
private final RequestTypeEnum myRequestType;
private final IBaseResource myResource;
private final String myUrl;
BundleEntryParts(RequestTypeEnum theRequestType, String theUrl, IBaseResource theResource) {
super();
myRequestType = theRequestType;
myUrl = theUrl;
myResource = theResource;
}
public RequestTypeEnum getRequestType() {
return myRequestType;
}
public IBaseResource getResource() {
return myResource;
}
public String getUrl() {
return myUrl;
}
}
}

View File

@ -41,7 +41,7 @@ import static org.apache.commons.lang3.StringUtils.*;
public class FhirTerser {
public static final Pattern COMPARTMENT_MATCHER_PATH = Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)");
private static final Pattern COMPARTMENT_MATCHER_PATH = Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)");
private FhirContext myContext;
public FhirTerser(FhirContext theContext) {
@ -53,7 +53,7 @@ public class FhirTerser {
if (theChildDefinition == null)
return null;
if (theCurrentList == null || theCurrentList.isEmpty())
return new ArrayList<>(Arrays.asList(theChildDefinition.getElementName()));
return new ArrayList<>(Collections.singletonList(theChildDefinition.getElementName()));
List<String> newList = new ArrayList<>(theCurrentList);
newList.add(theChildDefinition.getElementName());
return newList;
@ -86,10 +86,6 @@ public class FhirTerser {
return (IBaseExtension) theBaseHasModifierExtensions.addModifierExtension().setUrl(theUrl);
}
private ExtensionDt createEmptyModifierExtensionDt(IBaseExtension theBaseExtension, String theUrl) {
return createEmptyExtensionDt(theBaseExtension, true, theUrl);
}
private ExtensionDt createEmptyModifierExtensionDt(ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, String theUrl) {
return createEmptyExtensionDt(theSupportsUndeclaredExtensions, true, theUrl);
}
@ -162,9 +158,9 @@ public class FhirTerser {
* @return Returns a list of all matching elements
*/
public <T extends IBase> List<T> getAllPopulatedChildElementsOfType(IBaseResource theResource, final Class<T> theType) {
final ArrayList<T> retVal = new ArrayList<T>();
final ArrayList<T> retVal = new ArrayList<>();
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource);
visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, new IModelVisitor() {
visit(new IdentityHashMap<>(), theResource, theResource, null, null, def, new IModelVisitor() {
@SuppressWarnings("unchecked")
@Override
public void acceptElement(IBaseResource theOuterResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
@ -181,9 +177,9 @@ public class FhirTerser {
}
public List<ResourceReferenceInfo> getAllResourceReferences(final IBaseResource theResource) {
final ArrayList<ResourceReferenceInfo> retVal = new ArrayList<ResourceReferenceInfo>();
final ArrayList<ResourceReferenceInfo> retVal = new ArrayList<>();
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource);
visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, new IModelVisitor() {
visit(new IdentityHashMap<>(), theResource, theResource, null, null, def, new IModelVisitor() {
@Override
public void acceptElement(IBaseResource theOuterResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
if (theElement == null || theElement.isEmpty()) {
@ -210,14 +206,12 @@ public class FhirTerser {
public BaseRuntimeChildDefinition getDefinition(Class<? extends IBaseResource> theResourceType, String thePath) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResourceType);
BaseRuntimeElementCompositeDefinition<?> currentDef = def;
List<String> parts = Arrays.asList(thePath.split("\\."));
List<String> subList = parts.subList(1, parts.size());
if (subList.size() < 1) {
throw new ConfigurationException("Invalid path: " + thePath);
}
return getDefinition(currentDef, subList);
return getDefinition(def, subList);
}
@ -237,11 +231,10 @@ public class FhirTerser {
}
BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def;
Object currentObj = theTarget;
List<String> parts = parsePath(currentDef, thePath);
List<T> retVal = getValues(currentDef, currentObj, parts, theWantedType);
List<T> retVal = getValues(currentDef, theTarget, parts, theWantedType);
if (retVal.isEmpty()) {
return null;
}
@ -649,7 +642,8 @@ public class FhirTerser {
wantType = matcher.group(2);
}
for (IBaseReference nextValue : getValues(theSource, nextPath, IBaseReference.class)) {
List<IBaseReference> values = getValues(theSource, nextPath, IBaseReference.class);
for (IBaseReference nextValue : values) {
IIdType nextTargetId = nextValue.getReferenceElement();
String nextRef = nextTargetId.toUnqualifiedVersionless().getValue();
@ -669,7 +663,8 @@ public class FhirTerser {
}
if (isNotBlank(wantType)) {
if (!nextTargetId.getResourceType().equals(wantType)) {
String nextTargetIdResourceType = nextTargetId.getResourceType();
if (nextTargetIdResourceType == null || !nextTargetIdResourceType.equals(wantType)) {
continue;
}
}
@ -692,104 +687,95 @@ public class FhirTerser {
theContainingElementPath.add(theElement);
theElementDefinitionPath.add(theDefinition);
theCallback.acceptElement(theElement, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath),
boolean recurse = theCallback.acceptElement(theElement, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath),
Collections.unmodifiableList(theElementDefinitionPath));
if (recurse) {
/*
* Visit undeclared extensions
*/
if (theElement instanceof ISupportsUndeclaredExtensions) {
ISupportsUndeclaredExtensions containingElement = (ISupportsUndeclaredExtensions) theElement;
for (ExtensionDt nextExt : containingElement.getUndeclaredExtensions()) {
theContainingElementPath.add(nextExt);
theCallback.acceptUndeclaredExtension(nextExt, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
theContainingElementPath.remove(theContainingElementPath.size() - 1);
/*
* Visit undeclared extensions
*/
if (theElement instanceof ISupportsUndeclaredExtensions) {
ISupportsUndeclaredExtensions containingElement = (ISupportsUndeclaredExtensions) theElement;
for (ExtensionDt nextExt : containingElement.getUndeclaredExtensions()) {
theContainingElementPath.add(nextExt);
theCallback.acceptUndeclaredExtension(nextExt, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
theContainingElementPath.remove(theContainingElementPath.size() - 1);
}
}
}
/*
* Now visit the children of the given element
*/
switch (theDefinition.getChildType()) {
case ID_DATATYPE:
case PRIMITIVE_XHTML_HL7ORG:
case PRIMITIVE_XHTML:
case PRIMITIVE_DATATYPE:
// These are primitive types, so we don't need to visit their children
break;
case RESOURCE:
case RESOURCE_BLOCK:
case COMPOSITE_DATATYPE: {
BaseRuntimeElementCompositeDefinition<?> childDef = (BaseRuntimeElementCompositeDefinition<?>) theDefinition;
for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) {
List<? extends IBase> values = nextChild.getAccessor().getValues(theElement);
if (values != null) {
for (IBase nextValue : values) {
if (nextValue == null) {
continue;
}
if (nextValue.isEmpty()) {
continue;
}
BaseRuntimeElementDefinition<?> childElementDef;
childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass());
if (childElementDef == null) {
StringBuilder b = new StringBuilder();
b.append("Found value of type[");
b.append(nextValue.getClass().getSimpleName());
b.append("] which is not valid for field[");
b.append(nextChild.getElementName());
b.append("] in ");
b.append(childDef.getName());
b.append(" - Valid types: ");
for (Iterator<String> iter = new TreeSet<String>(nextChild.getValidChildNames()).iterator(); iter.hasNext(); ) {
BaseRuntimeElementDefinition<?> childByName = nextChild.getChildByName(iter.next());
b.append(childByName.getImplementingClass().getSimpleName());
if (iter.hasNext()) {
b.append(", ");
}
/*
* Now visit the children of the given element
*/
switch (theDefinition.getChildType()) {
case ID_DATATYPE:
case PRIMITIVE_XHTML_HL7ORG:
case PRIMITIVE_XHTML:
case PRIMITIVE_DATATYPE:
// These are primitive types, so we don't need to visit their children
break;
case RESOURCE:
case RESOURCE_BLOCK:
case COMPOSITE_DATATYPE: {
BaseRuntimeElementCompositeDefinition<?> childDef = (BaseRuntimeElementCompositeDefinition<?>) theDefinition;
for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) {
List<? extends IBase> values = nextChild.getAccessor().getValues(theElement);
if (values != null) {
for (IBase nextValue : values) {
if (nextValue == null) {
continue;
}
if (nextValue.isEmpty()) {
continue;
}
BaseRuntimeElementDefinition<?> childElementDef;
childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass());
if (childElementDef == null) {
StringBuilder b = new StringBuilder();
b.append("Found value of type[");
b.append(nextValue.getClass().getSimpleName());
b.append("] which is not valid for field[");
b.append(nextChild.getElementName());
b.append("] in ");
b.append(childDef.getName());
b.append(" - Valid types: ");
for (Iterator<String> iter = new TreeSet<>(nextChild.getValidChildNames()).iterator(); iter.hasNext(); ) {
BaseRuntimeElementDefinition<?> childByName = nextChild.getChildByName(iter.next());
b.append(childByName.getImplementingClass().getSimpleName());
if (iter.hasNext()) {
b.append(", ");
}
}
throw new DataFormatException(b.toString());
}
throw new DataFormatException(b.toString());
}
if (nextChild instanceof RuntimeChildDirectResource) {
// Don't descend into embedded resources
theContainingElementPath.add(nextValue);
theChildDefinitionPath.add(nextChild);
theElementDefinitionPath.add(myContext.getElementDefinition(nextValue.getClass()));
theCallback.acceptElement(nextValue, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath),
Collections.unmodifiableList(theElementDefinitionPath));
theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1);
theContainingElementPath.remove(theContainingElementPath.size() - 1);
theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1);
} else {
visit(nextValue, nextChild, childElementDef, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
}
}
}
break;
}
break;
}
case CONTAINED_RESOURCES: {
BaseContainedDt value = (BaseContainedDt) theElement;
for (IResource next : value.getContainedResources()) {
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(next);
visit(next, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
case CONTAINED_RESOURCES: {
BaseContainedDt value = (BaseContainedDt) theElement;
for (IResource next : value.getContainedResources()) {
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(next);
visit(next, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
}
break;
}
break;
}
case EXTENSION_DECLARED:
case UNDECL_EXT: {
throw new IllegalStateException("state should not happen: " + theDefinition.getChildType());
}
case CONTAINED_RESOURCE_LIST: {
if (theElement != null) {
BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
visit(theElement, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
case EXTENSION_DECLARED:
case UNDECL_EXT: {
throw new IllegalStateException("state should not happen: " + theDefinition.getChildType());
}
case CONTAINED_RESOURCE_LIST: {
if (theElement != null) {
BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
visit(theElement, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
}
break;
}
break;
}
}
if (theChildDefinition != null) {
@ -812,14 +798,14 @@ public class FhirTerser {
*/
public void visit(IBaseResource theResource, IModelVisitor theVisitor) {
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource);
visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, theVisitor);
visit(new IdentityHashMap<>(), theResource, theResource, null, null, def, theVisitor);
}
/**
* Visit all elements in a given resource
* <p>
* THIS ALTERNATE METHOD IS STILL EXPERIMENTAL
*
* <b>THIS ALTERNATE METHOD IS STILL EXPERIMENTAL! USE WITH CAUTION</b>
* </p>
* <p>
* Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g.
* {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource)
@ -828,9 +814,9 @@ public class FhirTerser {
* @param theResource The resource to visit
* @param theVisitor The visitor
*/
void visit(IBaseResource theResource, IModelVisitor2 theVisitor) {
public void visit(IBaseResource theResource, IModelVisitor2 theVisitor) {
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource);
visit(theResource, null, def, theVisitor, new ArrayList<IBase>(), new ArrayList<BaseRuntimeChildDefinition>(), new ArrayList<BaseRuntimeElementDefinition<?>>());
visit(theResource, null, def, theVisitor, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}
private void visit(IdentityHashMap<Object, Object> theStack, IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition,
@ -924,4 +910,66 @@ public class FhirTerser {
}
/**
* Returns all embedded resources that are found embedded within <code>theResource</code>.
* An embedded resource is a resource that can be found as a direct child within a resource,
* as opposed to being referenced by the resource.
* <p>
* Examples include resources found within <code>Bundle.entry.resource</code>
* and <code>Parameters.parameter.resource</code>, as well as contained resources
* found within <code>Resource.contained</code>
* </p>
*
* @param theRecurse Should embedded resources be recursively scanned for further embedded
* resources
* @return A collection containing the embedded resources. Order is arbitrary.
*/
public Collection<IBaseResource> getAllEmbeddedResources(IBaseResource theResource, boolean theRecurse) {
Validate.notNull(theResource, "theResource must not be null");
ArrayList<IBaseResource> retVal = new ArrayList<>();
visit(theResource, new IModelVisitor2() {
@Override
public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
if (theElement == theResource) {
return true;
}
if (theElement instanceof IBaseResource) {
retVal.add((IBaseResource) theElement);
return theRecurse;
}
return true;
}
@Override
public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
return true;
}
});
return retVal;
}
/**
* Clear all content on a resource
*/
public void clear(IBaseResource theInput) {
visit(theInput, new IModelVisitor2() {
@Override
public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
if (theElement instanceof IPrimitiveType) {
((IPrimitiveType) theElement).setValueAsString(null);
}
return true;
}
@Override
public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
theNextExt.setUrl(null);
theNextExt.setValue(null);
return true;
}
});
}
}

View File

@ -20,30 +20,32 @@ package ca.uhn.fhir.util;
* #L%
*/
import java.util.List;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import java.util.List;
/**
* THIS API IS EXPERIMENTAL IN HAPI FHIR - USE WITH CAUTION AS THE PUBLISHED API MAY
* CHANGE
*
* @see FhirTerser#visit(IBaseResource, IModelVisitor2)
*/
public interface IModelVisitor2 {
/**
* @param theElement The element being visited
* @param theElement The element being visited
* @param theContainingElementPath The elements in the path leading up to the actual element being accepted. The first element in this path will be the outer resource being visited, and the last element will be the saem object as the object passed as <code>theElement</code>
*/
boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath);
/**
*
*/
boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath);
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.demo;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
@ -23,6 +24,7 @@ import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
@ -83,6 +85,7 @@ public class JpaServerDemo extends RestfulServer {
} else if (fhirVersion == FhirVersionEnum.R4) {
systemProvider.add(myAppCtx.getBean("mySystemProviderR4", JpaSystemProviderR4.class));
systemProvider.add(myAppCtx.getBean(TerminologyUploaderProviderR4.class));
systemProvider.add(myAppCtx.getBean(BaseConfig.GRAPHQL_PROVIDER_NAME));
} else {
throw new IllegalStateException();
}

View File

@ -746,6 +746,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public IHistoryTyped andReturnBundle(Class theType) {
return returnBundle(theType);
}
@Override
public IHistoryTyped returnBundle(Class theType) {
Validate.notNull(theType, "theType must not be null on method andReturnBundle(Class)");
myReturnType = theType;
return this;

View File

@ -26,6 +26,9 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.commons.io.IOUtils;
@ -37,6 +40,7 @@ import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@Interceptor
public class LoggingInterceptor implements IClientInterceptor {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class);
@ -72,7 +76,7 @@ public class LoggingInterceptor implements IClientInterceptor {
}
}
@Override
@Hook(Pointcut.CLIENT_REQUEST)
public void interceptRequest(IHttpRequest theRequest) {
if (myLogRequestSummary) {
myLog.info("Client request: {}", theRequest);
@ -97,7 +101,7 @@ public class LoggingInterceptor implements IClientInterceptor {
}
}
@Override
@Hook(Pointcut.CLIENT_RESPONSE)
public void interceptResponse(IHttpResponse theResponse) throws IOException {
if (myLogResponseSummary) {
String message = "HTTP " + theResponse.getStatus() + " " + theResponse.getStatusInfo();

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@ -17,6 +18,7 @@ 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.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
@ -70,6 +72,7 @@ import javax.annotation.Nonnull;
public abstract class BaseConfig implements SchedulingConfigurer {
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider";
@Autowired
protected Environment myEnv;
@ -199,4 +202,9 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new HapiFhirHibernateJpaDialect(theLocalizer);
}
@Bean
public IConsentContextServices consentContextServices() {
return new JpaConsentContextServices();
}
}

View File

@ -86,7 +86,7 @@ public class BaseR4Config extends BaseConfig {
return new TransactionProcessor<>();
}
@Bean(name = "myGraphQLProvider")
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider() {
return new GraphQLProvider(fhirContextR4(), validationSupportChainR4(), graphqlStorageServices());

View File

@ -15,6 +15,7 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
@ -23,6 +24,7 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IResource;
@ -488,6 +490,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theProvider.setPlatformTransactionManager(myPlatformTransactionManager);
theProvider.setSearchDao(mySearchDao);
theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
theProvider.setInterceptorBroadcaster(myInterceptorBroadcaster);
}
public boolean isLogicalReference(IIdType theId) {
@ -1119,7 +1122,21 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
}
mySearchParamPresenceSvc.updatePresence(theEntity, presentSearchParams);
AddRemoveCount presenceCount = mySearchParamPresenceSvc.updatePresence(theEntity, presentSearchParams);
// Interceptor broadcast: JPA_PERFTRACE_INFO
if (!presenceCount.isEmpty()) {
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
StorageProcessingMessage message = new StorageProcessingMessage();
message.setMessage("For " + theEntity.getIdDt().toUnqualifiedVersionless().getValue() + " added " + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount() + " resource search parameter presence entries");
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, message);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
}
}
}
/*
@ -1129,7 +1146,24 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (newParams == null) {
myExpungeService.deleteAllSearchParams(theEntity.getId());
} else {
myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
// Synchronize search param indexes
AddRemoveCount searchParamAddRemoveCount = myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
// Interceptor broadcast: JPA_PERFTRACE_INFO
if (!searchParamAddRemoveCount.isEmpty()) {
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
StorageProcessingMessage message = new StorageProcessingMessage();
message.setMessage("For " + theEntity.getIdDt().toUnqualifiedVersionless().getValue() + " added " + searchParamAddRemoveCount.getAddCount() + " and removed " + searchParamAddRemoveCount.getRemoveCount() + " resource search parameter index entries");
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, message);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
}
}
// Syncrhonize composite params
mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams);
}
}

View File

@ -36,15 +36,14 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.*;
@ -523,7 +522,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource entity) {
List<TagDefinition> tags = toTagList(theMetaDel);
//@formatter:off
for (TagDefinition nextDef : tags) {
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
@ -534,7 +532,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
}
//@formatter:on
if (entity.getTags().isEmpty()) {
entity.setHasTags(false);
@ -571,18 +568,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return myExpungeService.expunge(getResourceName(), null, null, theExpungeOptions);
}
@Override
public TagList getAllResourceTags(RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
StopWatch w = new StopWatch();
TagList tags = super.getTags(theRequestDetails, myResourceType, null);
ourLog.debug("Processed getTags on {} in {}ms", myResourceName, w.getMillisAndRestart());
return tags;
}
public String getResourceName() {
return myResourceName;
}
@ -598,18 +583,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
myResourceType = (Class<T>) theTableType;
}
@Override
public TagList getTags(IIdType theResourceId, RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, null, theResourceId);
notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
StopWatch w = new StopWatch();
TagList retVal = super.getTags(theRequestDetails, myResourceType, theResourceId);
ourLog.debug("Processed getTags on {} in {}ms", theResourceId, w.getMillisAndRestart());
return retVal;
}
@Override
public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) {
// Notify interceptors
@ -933,12 +906,30 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
// Interceptor broadcast: STORAGE_PREACCESS_RESOURCES
{
SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal);
HookParams params = new HookParams()
.add(IBaseResource.class, retVal)
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
doCallHooks(theRequest, Pointcut.STORAGE_PREACCESS_RESOURCE, params);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
if (accessDetails.isDontReturnResourceAtIndex(0)) {
throw new ResourceNotFoundException(theId);
}
}
// Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
{
SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal);
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, showDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
//noinspection unchecked
retVal = (T) showDetails.getResource(0);
}
ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal;
@ -1125,10 +1116,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// FIXME: fail if too many results
HashSet<Long> retVal = new HashSet<Long>();
HashSet<Long> retVal = new HashSet<>();
String uuid = UUID.randomUUID().toString();
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(uuid);
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest)) {
while (iter.hasNext()) {
@ -1191,21 +1182,45 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
outcome.setId(id);
if (theEntity.getDeleted() == null) {
outcome.setResource(theResource);
}
outcome.setEntity(theEntity);
// Interceptor broadcast
HookParams params = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
doCallHooks(theRequest, Pointcut.STORAGE_PREACCESS_RESOURCE, params);
// Interceptor broadcast: STORAGE_PREACCESS_RESOURCES
if (outcome.getResource() != null) {
SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource());
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
if (accessDetails.isDontReturnResourceAtIndex(0)) {
outcome.setResource(null);
}
}
// Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
// Note that this will only fire if someone actually goes to use the
// resource in a response (it's their responsibility to call
// outcome.fireResourceViewCallback())
outcome.registerResourceViewCallback(()->{
if (outcome.getResource() != null) {
SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource());
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, showDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
outcome.setResource(showDetails.getResource(0));
}
});
return outcome;
}
private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) {
ArrayList<TagDefinition> retVal = new ArrayList<TagDefinition>();
ArrayList<TagDefinition> retVal = new ArrayList<>();
for (IBaseCoding next : theMeta.getTag()) {
retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()));
@ -1234,7 +1249,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
if (param == null) {
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<String>(searchParams.keySet()));
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<>(searchParams.keySet()));
throw new InvalidRequestException(msg);
}
@ -1411,7 +1426,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
private void validateResourceType(BaseHasResource entity) {
validateResourceType(entity, myResourceName);
}

View File

@ -5,7 +5,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.Validate;

View File

@ -195,7 +195,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
Query luceneQuery = bool.createQuery();
// wrap Lucene query in a javax.persistence.Query
// wrap Lucene query in a javax.persistence.SqlQuery
FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, ResourceTable.class);
jpaQuery.setProjection("myId");

View File

@ -45,10 +45,7 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
@ -112,12 +109,8 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
ExpungeOutcome expunge(IIdType theIIdType, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
TagList getAllResourceTags(RequestDetails theRequestDetails);
Class<T> getResourceType();
TagList getTags(IIdType theResourceId, RequestDetails theRequestDetails);
IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails);
IBundleProvider history(IIdType theId, Date theSince, Date theUntil, RequestDetails theRequestDetails);

View File

@ -42,11 +42,10 @@ public interface ISearchBuilder {
Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest);
void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager,
FhirContext theContext, IDao theDao, RequestDetails theRequest);
void loadResourcesByPid(Collection<Long> thePids, Collection<Long> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation, RequestDetails theDetails);
Set<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
DateRangeParam theLastUpdated, String theSearchIdOrDescription);
DateRangeParam theLastUpdated, String theSearchIdOrDescription, RequestDetails theRequest);
/**
* How many results may be fetched at once

View File

@ -21,17 +21,16 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
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.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.entity.*;
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.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.util.StringNormalizer;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
@ -41,9 +40,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.term.VersionIndependentConcept;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
import ca.uhn.fhir.jpa.util.*;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
@ -53,6 +50,7 @@ import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -110,7 +108,7 @@ public class SearchBuilder implements ISearchBuilder {
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);
/**
* @see ISearchBuilder#loadResourcesByPid(Collection, List, Set, boolean, EntityManager, FhirContext, IDao, RequestDetails)
* See loadResourcesByPid
* for an explanation of why we use the constant 800
*/
private static final int MAXIMUM_PAGE_SIZE = 800;
@ -142,11 +140,7 @@ public class SearchBuilder implements ISearchBuilder {
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private MatchResourceUrlService myMatchResourceUrlService;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao;
@ -1847,8 +1841,8 @@ public class SearchBuilder implements ISearchBuilder {
return retVal;
}
private void doLoadPids(List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager, FhirContext theContext, IDao theDao,
Map<Long, Integer> thePosition, Collection<Long> thePids, RequestDetails theRequest) {
private void doLoadPids(Collection<Long> thePids, Collection<Long> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation,
Map<Long, Integer> thePosition, RequestDetails theRequest) {
// -- get the resource from the searchView
Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(thePids);
@ -1859,11 +1853,11 @@ public class SearchBuilder implements ISearchBuilder {
Long resourceId;
for (ResourceSearchView next : resourceSearchViewList) {
Class<? extends IBaseResource> resourceType = theContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
Class<? extends IBaseResource> resourceType = myContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
resourceId = next.getId();
IBaseResource resource = theDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation);
IBaseResource resource = myCallingDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation);
if (resource == null) {
ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion());
continue;
@ -1888,13 +1882,6 @@ public class SearchBuilder implements ISearchBuilder {
}
}
// Interceptor broadcast: STORAGE_PREACCESS_RESOURCE
HookParams params = new HookParams()
.add(IBaseResource.class, resource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCE, params);
theResourceListToPopulate.set(index, resource);
}
}
@ -1938,19 +1925,18 @@ public class SearchBuilder implements ISearchBuilder {
}
@Override
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation,
EntityManager entityManager, FhirContext context, IDao theDao, RequestDetails theRequest) {
if (theIncludePids.isEmpty()) {
public void loadResourcesByPid(Collection<Long> thePids, Collection<Long> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation, RequestDetails theDetails) {
if (thePids.isEmpty()) {
ourLog.debug("The include pids are empty");
// return;
}
// Dupes will cause a crash later anyhow, but this is expensive so only do it
// when running asserts
assert new HashSet<>(theIncludePids).size() == theIncludePids.size() : "PID list contains duplicates: " + theIncludePids;
assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids;
Map<Long, Integer> position = new HashMap<>();
for (Long next : theIncludePids) {
for (Long next : thePids) {
position.put(next, theResourceListToPopulate.size());
theResourceListToPopulate.add(null);
}
@ -1961,12 +1947,12 @@ public class SearchBuilder implements ISearchBuilder {
* if it's lots of IDs. I suppose maybe we should be doing this as a join anyhow
* but this should work too. Sigh.
*/
List<Long> pids = new ArrayList<>(theIncludePids);
List<Long> pids = new ArrayList<>(thePids);
for (int i = 0; i < pids.size(); i += MAXIMUM_PAGE_SIZE) {
int to = i + MAXIMUM_PAGE_SIZE;
to = Math.min(to, pids.size());
List<Long> pidsSubList = pids.subList(i, to);
doLoadPids(theResourceListToPopulate, theIncludedPids, theForHistoryOperation, entityManager, context, theDao, position, pidsSubList, theRequest);
doLoadPids(pidsSubList, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position, theDetails);
}
}
@ -1977,7 +1963,7 @@ public class SearchBuilder implements ISearchBuilder {
*/
@Override
public HashSet<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription) {
boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription, RequestDetails theRequest) {
if (theMatches.size() == 0) {
return new HashSet<>();
}
@ -2109,6 +2095,30 @@ public class SearchBuilder implements ISearchBuilder {
ourLog.info("Loaded {} {} in {} rounds and {} ms for search {}", allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart(), theSearchIdOrDescription);
// Interceptor call: STORAGE_PREACCESS_RESOURCES
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (allAdded.size() > 0) {
List<Long> includedPidList = new ArrayList<>(allAdded);
JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(includedPidList, ()->this);
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = includedPidList.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
Long value = includedPidList.remove(i);
if (value != null) {
theMatches.remove(value);
}
}
}
allAdded = new HashSet<>(includedPidList);
}
return allAdded;
}
@ -2428,16 +2438,18 @@ public class SearchBuilder implements ISearchBuilder {
public class IncludesIterator extends BaseIterator<Long> implements Iterator<Long> {
private final RequestDetails myRequest;
private Iterator<Long> myCurrentIterator;
private int myCurrentOffset;
private ArrayList<Long> myCurrentPids;
private Long myNext;
private int myPageSize = myDaoConfig.getEverythingIncludesFetchPageSize();
IncludesIterator(Set<Long> thePidSet) {
IncludesIterator(Set<Long> thePidSet, RequestDetails theRequest) {
myCurrentPids = new ArrayList<>(thePidSet);
myCurrentIterator = EMPTY_LONG_LIST.iterator();
myCurrentOffset = 0;
myRequest = theRequest;
}
private void fetchNext() {
@ -2460,7 +2472,7 @@ public class SearchBuilder implements ISearchBuilder {
myCurrentOffset = end;
Collection<Long> pidsToScan = myCurrentPids.subList(start, end);
Set<Include> includes = Collections.singleton(new Include("*", true));
Set<Long> newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid);
Set<Long> newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid, myRequest);
myCurrentIterator = newPids.iterator();
}
@ -2485,6 +2497,7 @@ public class SearchBuilder implements ISearchBuilder {
private final class QueryIterator extends BaseIterator<Long> implements IResultIterator {
private final SearchRuntimeDetails mySearchRuntimeDetails;
private final RequestDetails myRequest;
private boolean myFirst = true;
private IncludesIterator myIncludesIterator;
private Long myNext;
@ -2493,7 +2506,6 @@ public class SearchBuilder implements ISearchBuilder {
private SortSpec mySort;
private boolean myStillNeedToFetchIncludes;
private int mySkipCount = 0;
private final RequestDetails myRequest;
private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
mySearchRuntimeDetails = theSearchRuntimeDetails;
@ -2508,6 +2520,12 @@ public class SearchBuilder implements ISearchBuilder {
private void fetchNext() {
boolean haveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
try {
if (haveRawSqlHooks) {
CurrentThreadCaptureQueriesListener.startCapturing();
}
// If we don't have a query yet, create one
if (myResultsIterator == null) {
if (myMaxResultsToFetch == null) {
@ -2558,7 +2576,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myNext == null) {
if (myStillNeedToFetchIncludes) {
myIncludesIterator = new IncludesIterator(myPidSet);
myIncludesIterator = new IncludesIterator(myPidSet, myRequest);
myStillNeedToFetchIncludes = false;
}
if (myIncludesIterator != null) {
@ -2582,16 +2600,31 @@ public class SearchBuilder implements ISearchBuilder {
mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
} finally {
if (haveRawSqlHooks) {
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SqlQueryList.class, capturedQueries);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_RAW_SQL, params);
}
}
if (myFirst) {
HookParams params = new HookParams();
params.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params);
myFirst = false;
}
if (NO_MORE.equals(myNext)) {
HookParams params = new HookParams();
params.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params);
}

View File

@ -22,13 +22,18 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.config.HapiFhirHibernateJpaDialect;
import ca.uhn.fhir.jpa.delete.DeleteConflictList;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
@ -94,6 +99,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect;
@Autowired
private DeleteConflictService myDeleteConflictService;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) {
@ -180,9 +187,10 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
if (theRequestDetails != null) {
if (outcome.getResource() != null) {
String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(null, prefer);
if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) {
outcome.fireResourceViewCallbacks();
myVersionAdapter.setResource(newEntry, outcome.getResource());
}
}
@ -438,7 +446,11 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
try {
IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails);
BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
requestDetails.setRestOperationType(methodBinding.getRestOperationType());
IBaseResource resource = methodBinding.doInvokeServer(theRequestDetails.getServer(), requestDetails);
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
@ -455,7 +467,17 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
}
transactionStopWatch.endCurrentTask();
ourLog.debug("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations());
// Interceptor broadcast: JPA_PERFTRACE_INFO
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) {
String taskDurations = transactionStopWatch.formatTaskDurations();
StorageProcessingMessage message = new StorageProcessingMessage();
message.setMessage("Transaction timing:\n" + taskDurations);
HookParams params = new HookParams()
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(StorageProcessingMessage.class, message);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_INFO, params);
}
return response;
}
@ -778,7 +800,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
boolean sourceStillReferencesTarget = referencesInSource
.stream()
.anyMatch(t-> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
if (!sourceStillReferencesTarget) {
iter.remove();
}

View File

@ -39,7 +39,7 @@ public interface ISearchDao extends JpaRepository<Search, Long> {
@Query("SELECT s.myId FROM Search s WHERE s.mySearchLastReturned < :cutoff")
Slice<Long> findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, Pageable thePage);
// @Query("SELECT s FROM Search s WHERE s.myCreated < :cutoff")
// @SqlQuery("SELECT s FROM Search s WHERE s.myCreated < :cutoff")
// public Collection<Search> findWhereCreatedBefore(@Param("cutoff") Date theCutoff);
@Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND s.myCreated > :cutoff AND s.myDeleted = false")

View File

@ -49,8 +49,7 @@ public class FhirResourceDaoCompositionDstu3 extends FhirResourceDaoDstu3<Compos
if (theId != null) {
paramMap.add("_id", new StringParam(theId.getIdPart()));
}
IBundleProvider bundleProvider = search(paramMap);
return bundleProvider;
return search(paramMap, theRequestDetails);
}
}

View File

@ -139,7 +139,7 @@ public class ExpungeEverythingService {
private int doExpungeEverythingQuery(String theQuery) {
StopWatch sw = new StopWatch();
int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
ourLog.debug("Query affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
return outcome;
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -41,22 +42,25 @@ public class DaoSearchParamSynchronizer {
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
public void synchronizeSearchParamsToDatabase(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
public AddRemoveCount synchronizeSearchParamsToDatabase(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
AddRemoveCount retVal = new AddRemoveCount();
synchronize(theParams, theEntity, theParams.myStringParams, existingParams.myStringParams);
synchronize(theParams, theEntity, theParams.myTokenParams, existingParams.myTokenParams);
synchronize(theParams, theEntity, theParams.myNumberParams, existingParams.myNumberParams);
synchronize(theParams, theEntity, theParams.myQuantityParams, existingParams.myQuantityParams);
synchronize(theParams, theEntity, theParams.myDateParams, existingParams.myDateParams);
synchronize(theParams, theEntity, theParams.myUriParams, existingParams.myUriParams);
synchronize(theParams, theEntity, theParams.myCoordsParams, existingParams.myCoordsParams);
synchronize(theParams, theEntity, theParams.myLinks, existingParams.myLinks);
synchronize(theParams, theEntity, retVal, theParams.myStringParams, existingParams.myStringParams);
synchronize(theParams, theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams);
synchronize(theParams, theEntity,retVal, theParams.myNumberParams, existingParams.myNumberParams);
synchronize(theParams, theEntity,retVal, theParams.myQuantityParams, existingParams.myQuantityParams);
synchronize(theParams, theEntity,retVal, theParams.myDateParams, existingParams.myDateParams);
synchronize(theParams, theEntity,retVal, theParams.myUriParams, existingParams.myUriParams);
synchronize(theParams, theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams);
synchronize(theParams, theEntity,retVal, theParams.myLinks, existingParams.myLinks);
// make sure links are indexed
theEntity.setResourceLinks(theParams.myLinks);
return retVal;
}
private <T extends BaseResourceIndex> void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Collection<T> theNewParms, Collection<T> theExistingParms) {
private <T extends BaseResourceIndex> void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection<T> theNewParms, Collection<T> theExistingParms) {
theParams.calculateHashes(theNewParms);
List<T> quantitiesToRemove = subtract(theExistingParms, theNewParms);
List<T> quantitiesToAdd = subtract(theNewParms, theExistingParms);
@ -68,6 +72,9 @@ public class DaoSearchParamSynchronizer {
for (T next : quantitiesToAdd) {
myEntityManager.merge(next);
}
theAddRemoveCount.addToAddCount(quantitiesToAdd.size());
theAddRemoveCount.addToRemoveCount(quantitiesToRemove.size());
}
/**

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import org.apache.commons.lang3.Validate;
@ -105,6 +106,8 @@ public class IdHelperService {
StorageProcessingMessage msg = new StorageProcessingMessage()
.setMessage("This search uses unqualified resource IDs (an ID without a resource type). This is less efficient than using a qualified type.");
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, msg);
JpaInterceptorBroadcaster.doCallHooks(theInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);

View File

@ -49,8 +49,7 @@ public class FhirResourceDaoCompositionR4 extends FhirResourceDaoR4<Composition>
if (theId != null) {
paramMap.add("_id", new StringParam(theId.getIdPart()));
}
IBundleProvider bundleProvider = search(paramMap);
return bundleProvider;
return search(paramMap, theRequestDetails);
}
}

View File

@ -22,14 +22,22 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -43,8 +51,12 @@ public class MatchResourceUrlService {
private FhirContext myContext;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType, RequestDetails theRequest) {
StopWatch sw = new StopWatch();
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType);
SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef);
@ -59,7 +71,20 @@ public class MatchResourceUrlService {
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
}
return dao.searchForIds(paramMap, theRequest);
Set<Long> retVal = dao.searchForIds(paramMap, theRequest);
// Interceptor broadcast: JPA_PERFTRACE_INFO
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
StorageProcessingMessage message = new StorageProcessingMessage();
message.setMessage("Processed conditional resource URL with " + retVal.size() + " result(s) in " + sw.toString());
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, message);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
}
return retVal;
}

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.slf4j.Logger;
@ -99,7 +100,9 @@ public class DeleteConflictService {
// Notify Interceptors about pre-action call
HookParams hooks = new HookParams()
.add(DeleteConflictList.class, theDeleteConflicts);
.add(DeleteConflictList.class, theDeleteConflicts)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
return JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, hooks);
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
@ -41,7 +42,7 @@ import static org.apache.commons.lang3.StringUtils.left;
@Index(name = "IDX_SEARCH_LASTRETURNED", columnList = "SEARCH_LAST_RETURNED"),
@Index(name = "IDX_SEARCH_RESTYPE_HASHS", columnList = "RESOURCE_TYPE,SEARCH_QUERY_STRING_HASH,CREATED")
})
public class Search implements Serializable {
public class Search implements ICachedSearchDetails, Serializable {
@SuppressWarnings("WeakerAccess")
public static final int UUID_COLUMN_LENGTH = 36;
@ -312,4 +313,9 @@ public class Search implements Serializable {
public void setSearchParameterMap(SearchParameterMap theSearchParameterMap) {
mySearchParameterMap = SerializationUtils.serialize(theSearchParameterMap);
}
@Override
public void setCannotBeReused() {
mySearchQueryStringHash = null;
}
}

View File

@ -36,6 +36,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.utils.GraphQLEngine;
@ -106,7 +107,8 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
}
}
IBundleProvider response = dao.search(params);
RequestDetails requestDetails = (RequestDetails) theAppInfo;
IBundleProvider response = dao.search(params, requestDetails);
int size = response.size();
if (response.preferredPageSize() != null && response.preferredPageSize() < size) {
size = response.preferredPageSize();
@ -121,21 +123,27 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Resource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
RequestDetails requestDetails = (RequestDetails) theAppInfo;
assert requestDetails != null;
IIdType refId = getContext().getVersion().newIdType();
refId.setValue(theType + "/" + theId);
IFhirResourceDao<? extends IBaseResource> dao = getDao(theType);
BaseHasResource id = dao.readEntity(refId, requestDetails);
return (Resource) toResource(id, false);
return lookup(theAppInfo, refId);
}
@Override
public ReferenceResolution lookup(Object appInfo, Resource context, Reference reference) throws FHIRException {
private Resource lookup(Object theAppInfo, IIdType theRefId) {
IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType());
RequestDetails requestDetails = (RequestDetails) theAppInfo;
return (Resource) dao.read(theRefId, requestDetails, false);
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public ReferenceResolution lookup(Object theAppInfo, Resource theContext, Reference theReference) throws FHIRException {
IdType refId = new IdType(theReference.getReference());
Resource outcome = lookup(theAppInfo, refId);
if (outcome == null) {
return null;
}
return new ReferenceResolution(theContext, outcome);
}
@Transactional(propagation = Propagation.NEVER)
@Override

View File

@ -0,0 +1,6 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
public class JpaConsentContextServices implements IConsentContextServices {
}

View File

@ -0,0 +1,53 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.util.ICallable;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.concurrent.NotThreadSafe;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* THIS CLASS IS NOT THREAD SAFE
*/
@NotThreadSafe
public class JpaPreResourceAccessDetails implements IPreResourceAccessDetails {
private final List<Long> myResourcePids;
private final boolean[] myBlocked;
private final ICallable<ISearchBuilder> mySearchBuilderSupplier;
private List<IBaseResource> myResources;
public JpaPreResourceAccessDetails(List<Long> theResourcePids, ICallable<ISearchBuilder> theSearchBuilderSupplier) {
myResourcePids = theResourcePids;
myBlocked = new boolean[myResourcePids.size()];
mySearchBuilderSupplier = theSearchBuilderSupplier;
}
@Override
public int size() {
return myResourcePids.size();
}
@Override
public IBaseResource getResource(int theIndex) {
if (myResources == null) {
myResources = new ArrayList<>(myResourcePids.size());
// FIXME: JA don't call interceptors for this query
mySearchBuilderSupplier.call().loadResourcesByPid(myResourcePids, Collections.emptySet(), myResources, false, null);
}
return myResources.get(theIndex);
}
@Override
public void setDontReturnResourceAtIndex(int theIndex) {
myBlocked[theIndex] = true;
}
public boolean isDontReturnResourceAtIndex(int theIndex) {
return myBlocked[theIndex];
}
}

View File

@ -24,11 +24,15 @@ import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
/**
* Logs details about the executed query
*/
@Interceptor()
public class PerformanceTracingLoggingInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(PerformanceTracingLoggingInterceptor.class);
@ -57,22 +61,32 @@ public class PerformanceTracingLoggingInterceptor {
@Hook(value = Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE)
public void searchSelectComplete(SearchRuntimeDetails theOutcome) {
log("Query found {} matches in {} for query {}", theOutcome.getFoundMatchesCount(), theOutcome.getQueryStopwatch(), theOutcome.getSearchUuid());
log("SqlQuery found {} matches in {} for query {}", theOutcome.getFoundMatchesCount(), theOutcome.getQueryStopwatch(), theOutcome.getSearchUuid());
}
@Hook(value = Pointcut.JPA_PERFTRACE_SEARCH_COMPLETE)
public void searchComplete(SearchRuntimeDetails theOutcome) {
log("Query {} is complete in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
log("SqlQuery {} is complete in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
}
@Hook(value = Pointcut.JPA_PERFTRACE_SEARCH_PASS_COMPLETE)
public void searchPassComplete(SearchRuntimeDetails theOutcome) {
log("Query {} pass complete and set to status {} in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getSearchStatus(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
log("SqlQuery {} pass complete and set to status {} in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getSearchStatus(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
}
@Hook(value = Pointcut.JPA_PERFTRACE_SEARCH_FAILED)
public void searchFailed(SearchRuntimeDetails theOutcome) {
log("Query {} failed in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
log("SqlQuery {} failed in {} - Found {} matches", theOutcome.getSearchUuid(), theOutcome.getQueryStopwatch(), theOutcome.getFoundMatchesCount());
}
@Hook(value = Pointcut.JPA_PERFTRACE_INFO)
public void info(StorageProcessingMessage theMessage) {
log("[INFO] " + theMessage);
}
@Hook(value = Pointcut.JPA_PERFTRACE_WARNING)
public void warning(StorageProcessingMessage theMessage) {
log("[WARNING] " + theMessage);
}
private void log(String theMessage, Object... theArgs) {

View File

@ -44,10 +44,7 @@ public class BaseJpaResourceProviderCompositionR4 extends JpaResourceProviderR4<
/**
* Composition/123/$document
*
* @param theRequestDetails
*/
//@formatter:off
@Operation(name = JpaConstants.OPERATION_DOCUMENT, idempotent = true, bundleType=BundleTypeEnum.DOCUMENT)
public IBaseBundle getDocumentForComposition(
@ -69,7 +66,6 @@ public class BaseJpaResourceProviderCompositionR4 extends JpaResourceProviderR4<
RequestDetails theRequestDetails
) {
//@formatter:on
startRequest(theServletRequest);
try {

View File

@ -53,9 +53,9 @@ public class DatabaseBackedPagingProvider extends BasePagingProvider implements
}
@Override
public synchronized IBundleProvider retrieveResultList(String theId, RequestDetails theRequest) {
public synchronized IBundleProvider retrieveResultList(RequestDetails theRequestDetails, String theId) {
IFhirSystemDao<?, ?> systemDao = myDaoRegistry.getSystemDao();
PersistedJpaBundleProvider provider = new PersistedJpaBundleProvider(theRequest, theId, systemDao);
PersistedJpaBundleProvider provider = new PersistedJpaBundleProvider(theRequestDetails, theId, systemDao);
if (!provider.ensureSearchEntityLoaded()) {
return null;
}
@ -63,7 +63,7 @@ public class DatabaseBackedPagingProvider extends BasePagingProvider implements
}
@Override
public synchronized String storeResultList(IBundleProvider theList) {
public synchronized String storeResultList(RequestDetails theRequestDetails, IBundleProvider theList) {
String uuid = theList.getUuid();
return uuid;
}

View File

@ -26,14 +26,15 @@ import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nullable;
import java.util.List;
public interface ISearchCoordinatorSvc {
void cancelAllActiveSearches();
List<Long> getResources(String theUuid, int theFrom, int theTo, RequestDetails theRequest);
List<Long> getResources(String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails);
IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequest);
IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, @Nullable RequestDetails theRequestDetails);
}

View File

@ -21,16 +21,20 @@ package ca.uhn.fhir.jpa.search;
*/
import ca.uhn.fhir.context.FhirContext;
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.IDao;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,6 +52,7 @@ import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
import java.util.stream.Collectors;
public class PersistedJpaBundleProvider implements IBundleProvider {
@ -62,6 +67,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
private Search mySearchEntity;
private String myUuid;
private boolean myCacheHit;
private IInterceptorBroadcaster myInterceptorBroadcaster;
public PersistedJpaBundleProvider(RequestDetails theRequest, String theSearchUuid, IDao theDao) {
myRequest = theRequest;
@ -105,7 +111,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
}
if (predicates.size() > 0) {
q.where(predicates.toArray(new Predicate[predicates.size()]));
q.where(predicates.toArray(new Predicate[0]));
}
q.orderBy(cb.desc(from.get("myUpdated")));
@ -127,6 +133,34 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
retVal.add(myDao.toResource(resource, true));
}
// Interceptor call: STORAGE_PREACCESS_RESOURCES
{
SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal);
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = retVal.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
retVal.remove(i);
}
}
}
// Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
{
SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal);
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, showDetails)
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
}
return retVal;
}
@ -280,18 +314,41 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
// Note: Leave as protected, HSPC depends on this
@SuppressWarnings("WeakerAccess")
protected List<IBaseResource> toResourceList(ISearchBuilder sb, List<Long> pidsSubList) {
protected List<IBaseResource> toResourceList(ISearchBuilder theSearchBuilder, List<Long> thePids) {
Set<Long> includedPids = new HashSet<>();
if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) {
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(theSearchBuilder.loadIncludes(myContext, myEntityManager, thePids, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid, myRequest));
includedPids.addAll(theSearchBuilder.loadIncludes(myContext, myEntityManager, thePids, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid, myRequest));
}
List<Long> includedPidList = new ArrayList<>(includedPids);
// Execute the query and make sure we return distinct results
List<IBaseResource> resources = new ArrayList<>();
sb.loadResourcesByPid(pidsSubList, resources, includedPids, false, myEntityManager, myContext, myDao, myRequest);
theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest);
// Interceptor call: STORAGE_PRESHOW_RESOURCE
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (resources.size() > 0) {
SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(resources);
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, accessDetails)
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
resources = resources
.stream()
.filter(t -> t != null)
.collect(Collectors.toList());
}
return resources;
}
public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) {
myInterceptorBroadcaster = theInterceptorBroadcaster;
}
}

View File

@ -20,20 +20,25 @@ package ca.uhn.fhir.jpa.search;
* #L%
*/
import java.util.List;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.dao.IDao;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.SearchTask;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider {
private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaSearchFirstPageBundleProvider.class);
@ -55,21 +60,41 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
ourLog.trace("Fetching search resource PIDs");
ourLog.trace("Fetching search resource PIDs from task: {}", mySearchTask.getClass());
final List<Long> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex);
ourLog.trace("Done fetching search resource PIDs");
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
List<IBaseResource> retVal = txTemplate.execute(theStatus -> toResourceList(mySearchBuilder, pids));
int totalCountWanted = theToIndex - theFromIndex;
if (retVal.size() < totalCountWanted) {
long totalCountWanted = theToIndex - theFromIndex;
long totalCountMatch = (int) retVal
.stream()
.filter(t -> !isInclude(t))
.count();
if (totalCountMatch < totalCountWanted) {
if (mySearch.getStatus() == SearchStatusEnum.PASSCMPLET) {
int remainingWanted = totalCountWanted - retVal.size();
int fromIndex = theToIndex - remainingWanted;
List<IBaseResource> remaining = super.getResources(fromIndex, theToIndex);
retVal.addAll(remaining);
/*
* This is a bit of complexity to account for the possibility that
* the consent service has filtered some results.
*/
Set<String> existingIds = retVal
.stream()
.map(t -> t.getIdElement().getValue())
.filter(t -> t != null)
.collect(Collectors.toSet());
long remainingWanted = totalCountWanted - totalCountMatch;
long fromIndex = theToIndex - remainingWanted;
List<IBaseResource> remaining = super.getResources((int) fromIndex, theToIndex);
remaining.forEach(t -> {
if (!existingIds.contains(t.getIdElement().getValue())) {
retVal.add(t);
}
});
}
}
ourLog.trace("Loaded resources to return");
@ -77,6 +102,13 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
return retVal;
}
private boolean isInclude(IBaseResource theResource) {
if (theResource instanceof IAnyResource) {
return "include".equals(ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(((IAnyResource) theResource)));
}
return "include".equals(ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(((IResource) theResource)));
}
@Override
public Integer size() {
ourLog.trace("Waiting for initial sync");

View File

@ -22,15 +22,19 @@ package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.context.FhirContext;
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.*;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.Include;
@ -39,6 +43,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
@ -47,6 +52,8 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
@ -78,7 +85,6 @@ import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ -159,7 +165,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
*/
@Override
@Transactional(propagation = Propagation.NEVER)
public List<Long> getResources(final String theUuid, int theFrom, int theTo, RequestDetails theRequest) {
public List<Long> getResources(final String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails) {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
@ -210,7 +216,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
String resourceType = search.getResourceType();
SearchParameterMap params = search.getSearchParameterMap();
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType);
SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequest);
SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequestDetails);
myIdToSearchTask.put(search.getUuid(), task);
myExecutor.submit(task);
}
@ -271,10 +277,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
theRetVal.setPlatformTransactionManager(myManagedTxManager);
theRetVal.setSearchDao(mySearchDao);
theRetVal.setSearchCoordinatorSvc(this);
theRetVal.setInterceptorBroadcaster(myInterceptorBroadcaster);
}
@Override
public IBundleProvider registerSearch(final IDao theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequest) {
public IBundleProvider registerSearch(final IDao theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
final String searchUuid = UUID.randomUUID().toString();
@ -302,7 +309,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) {
ourLog.debug("Search {} is loading in synchronous mode", searchUuid);
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(searchUuid);
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequestDetails, searchUuid);
searchRuntimeDetails.setLoadSynchronous(true);
// Execute the query and make sure we return distinct results
@ -313,7 +320,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
// Load the results synchronously
final List<Long> pids = new ArrayList<>();
try (IResultIterator resultIter = sb.createQuery(theParams, searchRuntimeDetails, theRequest)) {
try (IResultIterator resultIter = sb.createQuery(theParams, searchRuntimeDetails, theRequestDetails)) {
while (resultIter.hasNext()) {
pids.add(resultIter.next());
if (loadSynchronousUpTo != null && pids.size() >= loadSynchronousUpTo) {
@ -328,6 +335,19 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
throw new InternalErrorException(e);
}
JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(pids, () -> sb);
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = pids.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
pids.remove(i);
}
}
/*
* For synchronous queries, we load all the includes right away
* since we're returning a static bundle with all the results
@ -338,11 +358,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* individually for pages as we return them to clients
*/
final Set<Long> includedPids = new HashSet<>();
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)", theRequestDetails));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)", theRequestDetails));
List<Long> includedPidsList = new ArrayList<>(includedPids);
List<IBaseResource> resources = new ArrayList<>();
sb.loadResourcesByPid(pids, resources, includedPids, false, myEntityManager, myContext, theCallingDao, theRequest);
sb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
return new SimpleBundleProvider(resources);
});
}
@ -366,11 +387,23 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
PersistedJpaBundleProvider foundSearchProvider = txTemplate.execute(t -> {
Search searchToUse = null;
// Interceptor call: STORAGE_PRECHECK_FOR_CACHED_SEARCH
HookParams params = new HookParams()
.add(SearchParameterMap.class, theParams)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
Object outcome = JpaInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH, params);
if (Boolean.FALSE.equals(outcome)) {
return null;
}
// Check for a search matching the given hash
int hashCode = queryString.hashCode();
Collection<Search> candidates = mySearchDao.find(resourceType, hashCode, createdCutoff);
for (Search nextCandidateSearch : candidates) {
if (queryString.equals(nextCandidateSearch.getSearchQueryString())) {
searchToUse = nextCandidateSearch;
break;
}
}
@ -380,7 +413,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
searchToUse.setSearchLastReturned(new Date());
mySearchDao.updateSearchLastReturned(searchToUse.getId(), new Date());
retVal = new PersistedJpaBundleProvider(theRequest, searchToUse.getUuid(), theCallingDao);
retVal = new PersistedJpaBundleProvider(theRequestDetails, searchToUse.getUuid(), theCallingDao);
retVal.setCacheHit(true);
populateBundleProvider(retVal);
@ -399,11 +432,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
Search search = new Search();
populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search);
SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequest);
// Interceptor call: STORAGE_PRESEARCH_REGISTERED
HookParams params = new HookParams()
.add(ICachedSearchDetails.class, search)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequestDetails);
myIdToSearchTask.put(search.getUuid(), task);
myExecutor.submit(task);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, theCallingDao, task, sb, myManagedTxManager, theRequest);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, theCallingDao, task, sb, myManagedTxManager, theRequestDetails);
populateBundleProvider(retVal);
ourLog.debug("Search initial phase completed in {}ms", w.getMillis());
@ -411,6 +451,22 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
private void callInterceptorStoragePreAccessResources(IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequestDetails, ISearchBuilder theSb, List<Long> thePids) {
if (thePids.isEmpty() == false) {
JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(thePids, () -> theSb);
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = thePids.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
thePids.remove(i);
}
}
}
}
@VisibleForTesting
void setContextForUnitTest(FhirContext theCtx) {
myContext = theCtx;
@ -484,6 +540,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private boolean myAbortRequested;
private int myCountSavedTotal = 0;
private int myCountSavedThisPass = 0;
private int myCountBlockedThisPass = 0;
private boolean myAdditionalPrefetchThresholdsRemaining;
private List<Long> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch;
@ -498,7 +555,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myParams = theParams;
myResourceType = theResourceType;
myCompletionLatch = new CountDownLatch(1);
mySearchRuntimeDetails = new SearchRuntimeDetails(mySearch.getUuid());
mySearchRuntimeDetails = new SearchRuntimeDetails(theRequest, mySearch.getUuid());
mySearchRuntimeDetails.setQueryString(theParams.toNormalizedQueryString(theCallingDao.getContext()));
myRequest = theRequest;
}
@ -609,8 +666,30 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
doSaveSearch();
}
ArrayList<Long> unsyncedPids = myUnsyncedPids;
// Interceptor call: STORAGE_PREACCESS_RESOURCES
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (mySearchRuntimeDetails.getRequestDetails() != null && unsyncedPids.isEmpty() == false) {
JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(unsyncedPids, () -> newSearchBuilder());
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, mySearchRuntimeDetails.getRequestDetails())
.addIfMatchesType(ServletRequestDetails.class, mySearchRuntimeDetails.getRequestDetails());
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = unsyncedPids.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
unsyncedPids.remove(i);
myCountBlockedThisPass++;
myCountSavedTotal++;
}
}
}
List<SearchResult> resultsToSave = Lists.newArrayList();
for (Long nextPid : myUnsyncedPids) {
for (Long nextPid : unsyncedPids) {
SearchResult nextResult = new SearchResult(mySearch);
nextResult.setResourcePid(nextPid);
nextResult.setOrder(myCountSavedTotal);
@ -625,15 +704,15 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
mySearchResultDao.saveAll(resultsToSave);
synchronized (mySyncedPids) {
int numSyncedThisPass = myUnsyncedPids.size();
int numSyncedThisPass = unsyncedPids.size();
ourLog.trace("Syncing {} search results - Have more: {}", numSyncedThisPass, theResultIter.hasNext());
mySyncedPids.addAll(myUnsyncedPids);
myUnsyncedPids.clear();
mySyncedPids.addAll(unsyncedPids);
unsyncedPids.clear();
if (theResultIter.hasNext() == false) {
mySearch.setNumFound(myCountSavedTotal);
int skippedCount = theResultIter.getSkippedCount();
int totalFetched = skippedCount + myCountSavedThisPass;
int totalFetched = skippedCount + myCountSavedThisPass + myCountBlockedThisPass;
ourLog.trace("MaxToFetch[{}] SkippedCount[{}] CountSavedThisPass[{}] CountSavedThisTotal[{}] AdditionalPrefetchRemaining[{}]", myMaxResultsToFetch, skippedCount, myCountSavedThisPass, myCountSavedTotal, myAdditionalPrefetchThresholdsRemaining);
if (myMaxResultsToFetch != null && totalFetched < myMaxResultsToFetch) {
@ -719,14 +798,20 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
mySearchRuntimeDetails.setSearchStatus(mySearch.getStatus());
if (mySearch.getStatus() == SearchStatusEnum.FINISHED) {
HookParams params = new HookParams().add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_COMPLETE, params);
} else {
HookParams params = new HookParams().add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_PASS_COMPLETE, params);
}
ourLog.info("Have completed search for [{}{}] and found {} resources in {}ms - Status is {}", mySearch.getResourceType(), mySearch.getSearchQueryString(), mySyncedPids.size(), sw.getMillis(), mySearch.getStatus());
ourLog.trace("Have completed search for [{}{}] and found {} resources in {}ms - Status is {}", mySearch.getResourceType(), mySearch.getSearchQueryString(), mySyncedPids.size(), sw.getMillis(), mySearch.getStatus());
} catch (Throwable t) {
@ -764,7 +849,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
mySearch.setStatus(SearchStatusEnum.FAILED);
mySearchRuntimeDetails.setSearchStatus(mySearch.getStatus());
HookParams params = new HookParams().add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FAILED, params);
saveSearch();
@ -879,7 +967,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
/*
* Provide any PID we loaded in previous seasrch passes to the
* Provide any PID we loaded in previous search passes to the
* SearchBuilder so that we don't get duplicates coming from running
* the same query again.
*
@ -998,13 +1086,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* but keep the search going in the background (and have
* the next page of results ready to go when the client asks).
*/
public class SearchTask extends BaseTask {
class SearchTask extends BaseTask {
/**
* Constructor
*/
public SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest) {
super(theSearch, theCallingDao, theParams, theResourceType, theRequest);
SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails) {
super(theSearch, theCallingDao, theParams, theResourceType, theRequestDetails);
}
/**
@ -1012,7 +1100,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* will block until at least one page of results have been
* fetched from the DB, and will never block after that.
*/
public Integer awaitInitialSync() {
Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync");
do {
try {

View File

@ -305,7 +305,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME;
Date high = theJob.getThresholdHigh();
// Query for resources within threshold
// SqlQuery for resources within threshold
StopWatch pageSw = new StopWatch();
Slice<Long> range = myTxTemplate.execute(t -> {
PageRequest page = PageRequest.of(0, PASS_SIZE);

View File

@ -21,11 +21,12 @@ package ca.uhn.fhir.jpa.sp;
*/
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
import java.util.Map;
public interface ISearchParamPresenceSvc {
void updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence);
AddRemoveCount updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence);
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -40,9 +41,10 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
private DaoConfig myDaoConfig;
@Override
public void updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence) {
public AddRemoveCount updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence) {
AddRemoveCount retVal = new AddRemoveCount();
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
return;
return retVal;
}
Map<String, Boolean> presenceMap = new HashMap<>(theParamNameToPresence);
@ -77,6 +79,7 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
}
}
mySearchParamPresentDao.deleteAll(toDelete);
retVal.addToRemoveCount(toDelete.size());
// Add any that should be added
List<SearchParamPresent> toAdd = new ArrayList<>();
@ -86,7 +89,9 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
}
}
mySearchParamPresentDao.saveAll(toAdd);
retVal.addToRemoveCount(toAdd.size());
return retVal;
}
}

View File

@ -0,0 +1,48 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class AddRemoveCount {
private int myAddCount;
private int myRemoveCount;
public void addToAddCount(int theCount) {
myAddCount += theCount;
}
public void addToRemoveCount(int theCount) {
myRemoveCount += theCount;
}
public int getAddCount() {
return myAddCount;
}
public int getRemoveCount() {
return myRemoveCount;
}
public boolean isEmpty() {
return myAddCount > 0 || myRemoveCount > 0;
}
}

View File

@ -24,9 +24,7 @@ import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
@ -36,7 +34,7 @@ import static org.apache.commons.lang3.StringUtils.trim;
public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution {
private boolean myCaptureQueryStackTrace;
private boolean myCaptureQueryStackTrace = false;
/**
* This has an impact on performance! Use with caution.
@ -54,7 +52,11 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
@Override
public void execute(ExecutionInfo theExecutionInfo, List<QueryInfo> theQueryInfoList) {
final Queue<Query> queryList = provideQueryList();
final Queue<SqlQuery> queryList = provideQueryList();
if (queryList == null) {
return;
}
for (QueryInfo next : theQueryInfoList) {
String sql = trim(next.getQuery());
List<String> params;
@ -79,81 +81,10 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
long elapsedTime = theExecutionInfo.getElapsedTime();
long startTime = System.currentTimeMillis() - elapsedTime;
queryList.add(new Query(sql, params, startTime, elapsedTime, stackTraceElements, size));
queryList.add(new SqlQuery(sql, params, startTime, elapsedTime, stackTraceElements, size));
}
}
protected abstract Queue<Query> provideQueryList();
public static class Query {
private final String myThreadName = Thread.currentThread().getName();
private final String mySql;
private final List<String> myParams;
private final long myQueryTimestamp;
private final long myElapsedTime;
private final StackTraceElement[] myStackTrace;
private final int mySize;
Query(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime, StackTraceElement[] theStackTraceElements, int theSize) {
mySql = theSql;
myParams = Collections.unmodifiableList(theParams);
myQueryTimestamp = theQueryTimestamp;
myElapsedTime = theElapsedTime;
myStackTrace = theStackTraceElements;
mySize = theSize;
}
public long getQueryTimestamp() {
return myQueryTimestamp;
}
public long getElapsedTime() {
return myElapsedTime;
}
public String getThreadName() {
return myThreadName;
}
public String getSql(boolean theInlineParams, boolean theFormat) {
String retVal = mySql;
if (theFormat) {
retVal = new BasicFormatterImpl().format(retVal);
// BasicFormatterImpl annoyingly adds a newline at the very start of its output
while (retVal.startsWith("\n")) {
retVal = retVal.substring(1);
}
}
if (theInlineParams) {
List<String> nextParams = new ArrayList<>(myParams);
int idx = 0;
while (nextParams.size() > 0) {
idx = retVal.indexOf("?", idx);
if (idx == -1) {
break;
}
String nextSubstitution = "'" + nextParams.remove(0) + "'";
retVal = retVal.substring(0, idx) + nextSubstitution + retVal.substring(idx + 1);
idx += nextSubstitution.length();
}
}
if (mySize > 1) {
retVal += "\nsize: " + mySize + "\n";
}
return trim(retVal);
}
public StackTraceElement[] getStackTrace() {
return myStackTrace;
}
public int getSize() {
return mySize;
}
}
protected abstract Queue<SqlQuery> provideQueryList();
}

View File

@ -45,10 +45,10 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
private static final int CAPACITY = 1000;
private static final Logger ourLog = LoggerFactory.getLogger(CircularQueueCaptureQueriesListener.class);
private final Queue<Query> myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY));
private final Queue<SqlQuery> myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY));
@Override
protected Queue<Query> provideQueryList() {
protected Queue<SqlQuery> provideQueryList() {
return myQueries;
}
@ -63,20 +63,20 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
* Index 0 is oldest
*/
@SuppressWarnings("UseBulkOperation")
public List<Query> getCapturedQueries() {
public List<SqlQuery> getCapturedQueries() {
// Make a copy so that we aren't affected by changes to the list outside of the
// synchronized block
ArrayList<Query> retVal = new ArrayList<>(CAPACITY);
ArrayList<SqlQuery> retVal = new ArrayList<>(CAPACITY);
myQueries.forEach(retVal::add);
return Collections.unmodifiableList(retVal);
}
private List<Query> getQueriesForCurrentThreadStartingWith(String theStart) {
private List<SqlQuery> getQueriesForCurrentThreadStartingWith(String theStart) {
String threadName = Thread.currentThread().getName();
return getQueriesStartingWith(theStart, threadName);
}
private List<Query> getQueriesStartingWith(String theStart, String theThreadName) {
private List<SqlQuery> getQueriesStartingWith(String theStart, String theThreadName) {
return getCapturedQueries()
.stream()
.filter(t -> theThreadName == null || t.getThreadName().equals(theThreadName))
@ -84,63 +84,63 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
.collect(Collectors.toList());
}
private List<Query> getQueriesStartingWith(String theStart) {
private List<SqlQuery> getQueriesStartingWith(String theStart) {
return getQueriesStartingWith(theStart, null);
}
/**
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getSelectQueries() {
public List<SqlQuery> getSelectQueries() {
return getQueriesStartingWith("select");
}
/**
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getInsertQueries() {
public List<SqlQuery> getInsertQueries() {
return getQueriesStartingWith("insert");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getUpdateQueries() {
public List<SqlQuery> getUpdateQueries() {
return getQueriesStartingWith("update");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getDeleteQueries() {
public List<SqlQuery> getDeleteQueries() {
return getQueriesStartingWith("delete");
}
/**
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getSelectQueriesForCurrentThread() {
public List<SqlQuery> getSelectQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("select");
}
/**
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getInsertQueriesForCurrentThread() {
public List<SqlQuery> getInsertQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("insert");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getUpdateQueriesForCurrentThread() {
public List<SqlQuery> getUpdateQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("update");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getDeleteQueriesForCurrentThread() {
public List<SqlQuery> getDeleteQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("delete");
}
@ -195,7 +195,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
.findFirst()
.map(CircularQueueCaptureQueriesListener::formatQueryAsSql)
.orElse("NONE FOUND");
ourLog.info("First select Query:\n{}", firstSelectQuery);
ourLog.info("First select SqlQuery:\n{}", firstSelectQuery);
}
/**
@ -286,10 +286,10 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
}
private static String formatQueryAsSql(Query theQuery) {
private static String formatQueryAsSql(SqlQuery theQuery) {
String formattedSql = theQuery.getSql(true, true);
StringBuilder b = new StringBuilder();
b.append("Query at ");
b.append("SqlQuery at ");
b.append(new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString());
b.append(" took ").append(StopWatch.formatMillis(theQuery.getElapsedTime()));
b.append(" on Thread: ").append(theQuery.getThreadName());

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.ArrayDeque;
import java.util.Queue;
public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListener {
private static final ThreadLocal<Queue<SqlQuery>> ourQueues = new ThreadLocal<>();
@Override
protected Queue<SqlQuery> provideQueryList() {
return ourQueues.get();
}
/**
* Get the current queue of items and stop collecting
*/
public static SqlQueryList getCurrentQueueAndStopCapturing() {
Queue<SqlQuery> retVal = ourQueues.get();
ourQueues.remove();
if (retVal == null) {
return new SqlQueryList();
}
return new SqlQueryList(retVal);
}
/**
* Starts capturing queries for the current thread.
* <p>
* Note that you should strongly consider calling this in a
* try-finally block to ensure that you also call
* {@link #getCurrentQueueAndStopCapturing()} afterward. Otherwise
* this method is a potential memory leak!
* </p>
*/
public static void startCapturing() {
ourQueues.set(new ArrayDeque<>());
}
}

View File

@ -41,4 +41,26 @@ public class JpaInterceptorBroadcaster {
}
return retVal;
}
/**
* Broadcast hooks to both the interceptor service associated with the request, as well
* as the one associated with the JPA module.
*/
public static Object doCallHooksAndReturnObject(IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) {
Object retVal = true;
if (theInterceptorBroadcaster != null) {
retVal = theInterceptorBroadcaster.callHooksAndReturnObject(thePointcut, theParams);
}
if (theRequestDetails != null && retVal == null) {
retVal = theRequestDetails.getInterceptorBroadcaster().callHooksAndReturnObject(thePointcut, theParams);
}
return retVal;
}
public static boolean hasHooks(Pointcut thePointcut, IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequestDetails) {
if (theInterceptorBroadcaster != null && theInterceptorBroadcaster.hasHooks(thePointcut)) {
return true;
}
return theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster().hasHooks(thePointcut);
}
}

View File

@ -0,0 +1,109 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.util.UrlUtil;
import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.trim;
public class SqlQuery {
private final String myThreadName = Thread.currentThread().getName();
private final String mySql;
private final List<String> myParams;
private final long myQueryTimestamp;
private final long myElapsedTime;
private final StackTraceElement[] myStackTrace;
private final int mySize;
SqlQuery(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime, StackTraceElement[] theStackTraceElements, int theSize) {
mySql = theSql;
myParams = Collections.unmodifiableList(theParams);
myQueryTimestamp = theQueryTimestamp;
myElapsedTime = theElapsedTime;
myStackTrace = theStackTraceElements;
mySize = theSize;
}
public long getQueryTimestamp() {
return myQueryTimestamp;
}
public long getElapsedTime() {
return myElapsedTime;
}
public String getThreadName() {
return myThreadName;
}
public String getSql(boolean theInlineParams, boolean theFormat) {
return getSql(theInlineParams, theFormat, false);
}
public String getSql(boolean theInlineParams, boolean theFormat, boolean theSanitizeParams) {
String retVal = mySql;
if (theFormat) {
retVal = new BasicFormatterImpl().format(retVal);
// BasicFormatterImpl annoyingly adds a newline at the very start of its output
while (retVal.startsWith("\n")) {
retVal = retVal.substring(1);
}
}
if (theInlineParams) {
List<String> nextParams = new ArrayList<>(myParams);
int idx = 0;
while (nextParams.size() > 0) {
idx = retVal.indexOf("?", idx);
if (idx == -1) {
break;
}
String nextParamValue = nextParams.remove(0);
if (theSanitizeParams) {
nextParamValue = UrlUtil.sanitizeUrlPart(nextParamValue);
}
String nextSubstitution = "'" + nextParamValue + "'";
retVal = retVal.substring(0, idx) + nextSubstitution + retVal.substring(idx + 1);
idx += nextSubstitution.length();
}
}
if (mySize > 1) {
retVal += "\nsize: " + mySize + "\n";
}
return trim(retVal);
}
public StackTraceElement[] getStackTrace() {
return myStackTrace;
}
public int getSize() {
return mySize;
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.rest.api.server;
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR - Core Library
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
@ -20,16 +20,15 @@ package ca.uhn.fhir.rest.api.server;
* #L%
*/
import java.util.*;
import java.util.ArrayList;
import java.util.Queue;
import ca.uhn.fhir.context.FhirContext;
public interface IRequestDetails {
Map<String, String[]> getParameters();
Map<String, List<String>> getUnqualifiedToQualifiedNames();
FhirContext getFhirContext();
public class SqlQueryList extends ArrayList<SqlQuery> {
public SqlQueryList() {
super();
}
public SqlQueryList(Queue<SqlQuery> theList) {
super(theList);
}
}

View File

@ -25,12 +25,12 @@ public class BlockLargeNumbersOfParamsListener implements ProxyDataSourceBuilder
@Override
public void execute(ExecutionInfo theExecInfo, List<QueryInfo> theQueryInfoList) {
ourLog.trace("Query with {} queries", theQueryInfoList.size());
ourLog.trace("SqlQuery with {} queries", theQueryInfoList.size());
for (QueryInfo next : theQueryInfoList) {
ourLog.trace("Have {} param lists", next.getParametersList().size());
for (List<ParameterSetOperation> nextParamsList : next.getParametersList()) {
ourLog.trace("Have {} sub-param lists", nextParamsList.size());
Validate.isTrue(nextParamsList.size() < 1000, "Query has %s parameters: %s", nextParamsList.size(), next.getQuery());
Validate.isTrue(nextParamsList.size() < 1000, "SqlQuery has %s parameters: %s", nextParamsList.size(), next.getQuery());
}
}
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
@ -111,6 +112,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
.afterQuery(captureQueriesListener())
.afterQuery(new CurrentThreadCaptureQueriesListener())
.countQuery(new ThreadQueryCountHolder())
.build();

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
@ -106,6 +107,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS)
.afterQuery(captureQueriesListener())
.afterQuery(new CurrentThreadCaptureQueriesListener())
.countQuery()
.build();

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
@ -107,6 +108,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
// .countQuery(new ThreadQueryCountHolder())
.beforeQuery(new BlockLargeNumbersOfParamsListener())
.afterQuery(captureQueriesListener())
.afterQuery(new CurrentThreadCaptureQueriesListener())
.countQuery(singleQueryCountHolder())
.build();

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.hibernate.HibernateException;
import org.hibernate.Session;
@ -93,7 +94,7 @@ public abstract class BaseJpaTest {
@After
public void afterPerformCleanup() {
BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(false);
BaseHapiFhirDao.setDisableIncrementOnUpdateForUnitTest(false);
if (myCaptureQueriesListener != null) {
myCaptureQueriesListener.clear();
}
@ -256,7 +257,7 @@ public abstract class BaseJpaTest {
if (theFirstCall) {
bundleProvider = theFound;
} else {
bundleProvider = myDatabaseBackedPagingProvider.retrieveResultList(theFound.getUuid(), null);
bundleProvider = myDatabaseBackedPagingProvider.retrieveResultList(null, theFound.getUuid());
}
List<IBaseResource> resources = bundleProvider.getResources(theFromIndex, theToIndex);
@ -382,7 +383,7 @@ public abstract class BaseJpaTest {
return IOUtils.toString(bundleRes, Constants.CHARSET_UTF8);
}
public static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry) {
protected static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao<?, ?> theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry) {
theSearchCoordinatorSvc.cancelAllActiveSearches();
theResourceReindexingSvc.cancelAndPurgeAllJobs();
@ -411,7 +412,7 @@ public abstract class BaseJpaTest {
theSearchParamRegistry.forceRefresh();
}
public static Set<String> toCodes(Set<TermConcept> theConcepts) {
protected static Set<String> toCodes(Set<TermConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (TermConcept next : theConcepts) {
retVal.add(next.getCode());
@ -419,8 +420,8 @@ public abstract class BaseJpaTest {
return retVal;
}
public static Set<String> toCodes(List<VersionIndependentConcept> theConcepts) {
HashSet<String> retVal = new HashSet<String>();
protected static Set<String> toCodes(List<VersionIndependentConcept> theConcepts) {
HashSet<String> retVal = new HashSet<>();
for (VersionIndependentConcept next : theConcepts) {
retVal.add(next.getCode());
}
@ -453,20 +454,6 @@ public abstract class BaseJpaTest {
}
}
public static void waitForTrue(Supplier<Boolean> theList) {
StopWatch sw = new StopWatch();
while (!theList.get() && sw.getMillis() <= 16000) {
try {
Thread.sleep(50);
} catch (InterruptedException theE) {
throw new Error(theE);
}
}
if (sw.getMillis() >= 16000) {
fail("Waited " + sw.toString() + " and is still false");
}
}
public static void waitForSize(int theTarget, Callable<Number> theCallable) throws Exception {
waitForSize(theTarget, 10000, theCallable);
}

View File

@ -0,0 +1,490 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.ListUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static junit.framework.TestCase.assertTrue;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest {
private static final Logger ourLog = LoggerFactory.getLogger(ConsentEventsDaoR4Test.class);
private List<String> myObservationIds;
private List<String> myPatientIds;
private InterceptorService myInterceptorService;
private List<String> myObservationIdsOddOnly;
private List<String> myObservationIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyBackwards;
private List<String> myObservationIdsBackwards;
private List<String> myPatientIdsEvenOnly;
@After
public void after() {
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
}
@Override
@Before
public void before() throws ServletException {
super.before();
RestfulServer restfulServer = new RestfulServer();
restfulServer.setPagingProvider(myPagingProvider);
myInterceptorService = new InterceptorService();
when(mySrd.getInterceptorBroadcaster()).thenReturn(myInterceptorService);
when(mySrd.getServer()).thenReturn(restfulServer);
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
}
@Test
public void testSearchCountOnly() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCounting(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 10);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(0, 10), returnedIdValues);
assertEquals(1, hitCount.get());
assertEquals(myObservationIds.subList(0, 20), interceptedResourceIds);
// Fetch the next 30 (do cross a fetch boundary)
resources = outcome.getResources(10, 40);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(10, 40), returnedIdValues);
assertEquals(2, hitCount.get());
assertEquals(myObservationIds.subList(0, 50), interceptedResourceIds);
}
@Test
public void testSearchAndBlockSome() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 10);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(0, 10), returnedIdValues);
assertEquals(1, hitCount.get());
assertEquals(myObservationIds.subList(0, 20), interceptedResourceIds);
// Fetch the next 30 (do cross a fetch boundary)
resources = outcome.getResources(10, 40);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(10, 25), returnedIdValues);
assertEquals(2, hitCount.get());
}
@Test
public void testSearchAndBlockSome_LoadSynchronous() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 10);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(0, 10), returnedIdValues);
assertEquals(1, hitCount.get());
assertEquals(myObservationIds, interceptedResourceIds);
// Fetch the next 30 (do cross a fetch boundary)
resources = outcome.getResources(10, 40);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(10, 25), returnedIdValues);
assertEquals(1, hitCount.get());
}
@Test
public void testSearchAndBlockSomeOnRevIncludes() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
map.addRevInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myPatientDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 100);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(sort(myPatientIdsEvenOnly, myObservationIdsEvenOnly), sort(returnedIdValues));
assertEquals(2, hitCount.get());
}
@Test
public void testSearchAndBlockSomeOnRevIncludes_LoadSynchronous() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
map.addRevInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myPatientDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 100);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(sort(myPatientIdsEvenOnly, myObservationIdsEvenOnly), sort(returnedIdValues));
assertEquals(2, hitCount.get());
}
@Test
public void testSearchAndBlockSomeOnIncludes() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.addInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 100);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(sort(myPatientIdsEvenOnly, myObservationIdsEvenOnly), sort(returnedIdValues));
// This should probably be 4
assertEquals(5, hitCount.get());
}
@Test
public void testSearchAndBlockNoneOnIncludes() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCounting(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.addInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 100);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(sort(myPatientIds, myObservationIds), sort(returnedIdValues));
assertEquals(4, hitCount.get());
}
@Test
public void testSearchAndBlockSomeOnIncludes_LoadSynchronous() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a search
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.addInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myObservationDao.search(map, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 100);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(sort(myPatientIdsEvenOnly, myObservationIdsEvenOnly), sort(returnedIdValues));
assertEquals(2, hitCount.get());
}
@Test
public void testHistoryAndBlockSome() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
// Perform a history
SearchParameterMap map = new SearchParameterMap();
map.setSort(new SortSpec(Observation.SP_IDENTIFIER, SortOrderEnum.ASC));
IBundleProvider outcome = myObservationDao.history(null, null, mySrd);
ourLog.info("Search UUID: {}", outcome.getUuid());
// Fetch the first 10 (don't cross a fetch boundary)
List<IBaseResource> resources = outcome.getResources(0, 10);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
/*
* Note: Each observation in the observation list will appear twice in the actual
* returned results because we create it then update it in create50Observations()
*/
assertEquals(sort(myObservationIdsEvenOnlyBackwards.subList(0, 3), myObservationIdsEvenOnlyBackwards.subList(0, 3)), sort(returnedIdValues));
assertEquals(1, hitCount.get());
assertEquals(sort(myObservationIdsBackwards.subList(0, 5), myObservationIdsBackwards.subList(0, 5)), sort(interceptedResourceIds));
}
@Test
public void testReadAndBlockSome() {
create50Observations();
AtomicInteger hitCount = new AtomicInteger(0);
List<String> interceptedResourceIds = new ArrayList<>();
IAnonymousInterceptor interceptor = new PreAccessInterceptorCountingAndBlockOdd(hitCount, interceptedResourceIds);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
myObservationDao.read(new IdType(myObservationIdsEvenOnly.get(0)), mySrd);
myObservationDao.read(new IdType(myObservationIdsEvenOnly.get(1)), mySrd);
try {
myObservationDao.read(new IdType(myObservationIdsOddOnly.get(0)), mySrd);
fail();
} catch (ResourceNotFoundException e) {
// good
}
try {
myObservationDao.read(new IdType(myObservationIdsOddOnly.get(1)), mySrd);
fail();
} catch (ResourceNotFoundException e) {
// good
}
}
private void create50Observations() {
myPatientIds = new ArrayList<>();
myObservationIds = new ArrayList<>();
Patient p = new Patient();
p.setActive(true);
IIdType pid0 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
myPatientIds.add(pid0.getValue());
p = new Patient();
p.setActive(true);
IIdType pid1 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
myPatientIds.add(pid1.getValue());
assertTrue((pid0.getIdPartAsLong() % 2) != (pid1.getIdPartAsLong() % 2));
String evenPid = pid0.getIdPartAsLong() % 2 == 0 ? pid0.getValue() : pid1.getValue();
String oddPid = pid0.getIdPartAsLong() % 2 == 0 ? pid1.getValue() : pid0.getValue();
for (int i = 0; i < 50; i++) {
final Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.addIdentifier().setSystem("urn:system").setValue("I" + leftPad("" + i, 5, '0'));
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
myObservationIds.add(obs1id.toUnqualifiedVersionless().getValue());
obs1.setId(obs1id);
if (obs1id.getIdPartAsLong() % 2 == 0) {
obs1.getSubject().setReference(evenPid);
} else {
obs1.getSubject().setReference(oddPid);
}
myObservationDao.update(obs1);
}
myPatientIdsEvenOnly =
myPatientIds
.stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.collect(Collectors.toList());
myObservationIdsEvenOnly =
myObservationIds
.stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.collect(Collectors.toList());
myObservationIdsOddOnly = ListUtils.removeAll(myObservationIds, myObservationIdsEvenOnly);
myObservationIdsBackwards = Lists.reverse(myObservationIds);
myObservationIdsEvenOnlyBackwards = Lists.reverse(myObservationIdsEvenOnly);
}
static class PreAccessInterceptorCounting implements IAnonymousInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(PreAccessInterceptorCounting.class);
private final AtomicInteger myHitCount;
private final List<String> myInterceptedResourceIds;
PreAccessInterceptorCounting(AtomicInteger theHitCount, List<String> theInterceptedResourceIds) {
myHitCount = theHitCount;
myInterceptedResourceIds = theInterceptedResourceIds;
}
@Override
public void invoke(Pointcut thePointcut, HookParams theArgs) {
myHitCount.incrementAndGet();
IPreResourceAccessDetails accessDetails = theArgs.get(IPreResourceAccessDetails.class);
assertThat(accessDetails.size(), greaterThan(0));
List<String> currentPassIds = new ArrayList<>();
for (int i = 0; i < accessDetails.size(); i++) {
IBaseResource nextResource = accessDetails.getResource(i);
if (nextResource != null) {
currentPassIds.add(nextResource.getIdElement().toUnqualifiedVersionless().getValue());
}
}
ourLog.info("Call to STORAGE_PREACCESS_RESOURCES with {} IDs: {}", currentPassIds.size(), currentPassIds);
myInterceptedResourceIds.addAll(currentPassIds);
}
}
static class PreAccessInterceptorCountingAndBlockOdd extends PreAccessInterceptorCounting {
PreAccessInterceptorCountingAndBlockOdd(AtomicInteger theHitCount, List<String> theInterceptedResourceIds) {
super(theHitCount, theInterceptedResourceIds);
}
@Override
public void invoke(Pointcut thePointcut, HookParams theArgs) {
super.invoke(thePointcut, theArgs);
IPreResourceAccessDetails accessDetails = theArgs.get(IPreResourceAccessDetails.class);
List<String> nonBlocked = new ArrayList<>();
int count = accessDetails.size();
List<String> ids = new ArrayList<>();
for (int i = 0; i < accessDetails.size(); i++) {
ids.add(accessDetails.getResource(i).getIdElement().toUnqualifiedVersionless().getValue());
}
ourLog.info("Invoking {} for {} results: {}", thePointcut, count, ids);
for (int i = 0; i < count; i++) {
IBaseResource resource = accessDetails.getResource(i);
if (resource != null) {
long idPart = resource.getIdElement().getIdPartAsLong();
if (idPart % 2 == 1) {
accessDetails.setDontReturnResourceAtIndex(i);
} else {
nonBlocked.add(resource.getIdElement().toUnqualifiedVersionless().getValue());
}
}
}
ourLog.info("Allowing IDs: {}", nonBlocked);
try {
throw new Exception();
} catch (Exception e) {
ourLog.error("Trace", e);
}
}
}
private static List<String> sort(List<String>... theLists) {
ArrayList<String> retVal = new ArrayList<>();
for (List<String> next : theLists) {
retVal.addAll(next);
}
retVal.sort((o0, o1) -> {
long i0 = Long.parseLong(o0.substring(o0.indexOf('/') + 1));
long i1 = Long.parseLong(o1.substring(o1.indexOf('/') + 1));
return (int) (i0 - i1);
});
return retVal;
}
}

View File

@ -89,7 +89,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(200, results.size().intValue());
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, empty());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
}
@Test
@ -119,7 +119,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(201, results.size().intValue());
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, empty());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
// Seach with total expicitly requested
params = new SearchParameterMap();
@ -131,7 +131,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(201, results.size().intValue());
ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, hasSize(10));
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
// Seach with count only
params = new SearchParameterMap();
@ -143,7 +143,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(201, results.size().intValue());
ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, empty());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
}
@ -164,7 +164,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
String uuid = results.getUuid();
ourLog.info("** Search returned UUID: {}", uuid);
// assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid).size().intValue());
// assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(mySrd, uuid).size().intValue());
assertEquals(200, results.size().intValue());
ourLog.info("** Asking for results");
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 5, true);
@ -172,7 +172,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals("Patient/PT00004", ids.get(4));
ourLog.info("** About to make new query for search with UUID: {}", uuid);
IBundleProvider search2 = myDatabaseBackedPagingProvider.retrieveResultList(uuid, null);
IBundleProvider search2 = myDatabaseBackedPagingProvider.retrieveResultList(null, uuid);
Integer search2Size = search2.size();
assertEquals(200, search2Size.intValue());
}
@ -194,7 +194,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
// Try the same query again. This time the same thing should come back, but
// from the cache...
@ -210,7 +210,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
}
@ -229,7 +229,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertEquals(null, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertEquals(null, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
// Try the same query again. This time we'll request _total=accurate as well
// which means the total should be calculated no matter what.
@ -244,7 +244,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid2, null).size().intValue());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid2).size().intValue());
assertNotEquals(uuid, uuid2);
}
@ -267,7 +267,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 200, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00199", ids.get(199));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* 20 should be prefetched since that's the initial page size
@ -322,7 +322,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* 20 should be prefetched since that's the initial page size
@ -347,7 +347,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
ids = toUnqualifiedVersionlessIdValues(results, 10, 15, false);
assertEquals("Patient/PT00010", ids.get(0));
assertEquals("Patient/PT00014", ids.get(4));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* Search should be untouched
@ -387,7 +387,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
ids = toUnqualifiedVersionlessIdValues(results, 25, 30, false);
assertEquals("Patient/PT00025", ids.get(0));
assertEquals("Patient/PT00029", ids.get(4));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* Search should be untouched
@ -425,7 +425,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(10, ids.size());
assertEquals("Patient/PT00180", ids.get(0));
assertEquals("Patient/PT00189", ids.get(9));
assertEquals(190, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(190, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
}
@ -449,7 +449,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 50, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00049", ids.get(49));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* 20 should be prefetched since that's the initial page size
@ -484,7 +484,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9));
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size());
assertNull(myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size());
/*
* 20 should be prefetched since that's the initial page size
@ -509,7 +509,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
ids = toUnqualifiedVersionlessIdValues(results, 15, 25, false);
assertEquals("Patient/PT00015", ids.get(0));
assertEquals("Patient/PT00024", ids.get(9));
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(200, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
/*
* Search should be untouched
@ -619,7 +619,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(1, search.getVersion().intValue());
});
assertEquals(1, myDatabaseBackedPagingProvider.retrieveResultList(uuid, null).size().intValue());
assertEquals(1, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
}

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchBuilder;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -676,9 +675,10 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
assertEquals(1, mySearchParamRegistry.getActiveUniqueSearchParams("Observation").size());
myResourceReindexingSvc.markAllResourcesForReindexing();
myResourceReindexingSvc.forceReindexingPass();
myResourceReindexingSvc.forceReindexingPass();
myResourceReindexingSvc.markAllResourcesForReindexing("Observation");
assertEquals(1, myResourceReindexingSvc.forceReindexingPass());
assertEquals(0, myResourceReindexingSvc.forceReindexingPass());
assertEquals(0, myResourceReindexingSvc.forceReindexingPass());
uniques = myResourceIndexedCompositeStringUniqueDao.findAll();
assertEquals(uniques.toString(), 1, uniques.size());

View File

@ -0,0 +1,82 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.SqlQueryList;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Test;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;
@SuppressWarnings({"unchecked", "Duplicates"})
public class SearchWithInterceptorR4Test extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchWithInterceptorR4Test.class);
@Test
public void testRawSql_Search() {
IAnonymousInterceptor interceptor = (pointcut, params) -> {
RequestDetails requestDetails = params.get(RequestDetails.class);
SqlQueryList sqlQueries = params.get(SqlQueryList.class);
assertNotNull(requestDetails);
assertNotNull(sqlQueries);
SqlQueryList existing = (SqlQueryList) requestDetails.getUserData().get("QUERIES");
if (existing != null) {
existing.addAll(sqlQueries);
} else {
requestDetails.getUserData().put("QUERIES", sqlQueries);
}
};
try {
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.JPA_PERFTRACE_RAW_SQL, interceptor);
Patient patient = new Patient();
String patientId = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getValue();
Condition conditionS = new Condition();
conditionS.getCode().addCoding().setSystem("http://snomed.info/sct").setCode("123");
conditionS.getSubject().setReference(patientId);
myConditionDao.create(conditionS);
Condition conditionA = new Condition();
conditionA.getCode().addCoding().setSystem("http://snomed.info/sct").setCode("123");
conditionA.getAsserter().setReference(patientId);
myConditionDao.create(conditionA);
SearchParameterMap map = new SearchParameterMap();
map.add(Condition.SP_CODE, new TokenParam("http://snomed.info/sct", "123"));
IBundleProvider results = myConditionDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
assertEquals(2, ids.size());
SqlQueryList list = (SqlQueryList) mySrd.getUserData().get("QUERIES");
assertEquals(1, list.size());
String query = list.get(0).getSql(true, false);
ourLog.info("Query: {}", query);
assertThat(query, containsString("HASH_SYS_AND_VALUE in ('3788488238034018567')"));
} finally {
myInterceptorRegistry.unregisterInterceptor(interceptor);
}
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -68,12 +69,10 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
if (ourServer == null) {
ourRestServer = new RestfulServer(myFhirCtx);
ourRestServer.registerProviders(myResourceProviders.createProviders());
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
ourRestServer.registerProvider(mySystemProvider);
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(ourRestServer, mySystemDao, myDaoConfig);
confProvider.setImplementationDescription("THIS IS THE DESC");

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -86,10 +87,9 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
if (ourServer == null) {
ourRestServer = new RestfulServer(myFhirCtx);
ourRestServer.registerProviders(myResourceProviders.createProviders());
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderDstu3.class);
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider);

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.util.BaseCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.SqlQuery;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.instance.model.api.IIdType;
@ -38,7 +38,7 @@ public class ResourceProviderDeleteSqlDstu3Test extends BaseResourceProviderDstu
long deleteCount = myCaptureQueriesListener.getDeleteQueries()
.stream()
.filter(query -> query.getSql(false, false).contains("HFJ_SPIDX_TOKEN"))
.collect(Collectors.summarizingInt(BaseCaptureQueriesListener.Query::getSize))
.collect(Collectors.summarizingInt(SqlQuery::getSize))
.getSum();
assertEquals(1, deleteCount);
}

View File

@ -23,6 +23,8 @@ import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.junit.AfterClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
@ -33,6 +35,8 @@ import static org.junit.Assert.*;
public class AuthorizationInterceptorResourceProviderR4Test extends BaseResourceProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorResourceProviderR4Test.class);
@Override
public void before() throws Exception {
super.before();
@ -197,6 +201,60 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
}
@Test
public void testReadWithSubjectMasked() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType patientId = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
Observation obs = new Observation();
obs.setStatus(ObservationStatus.FINAL);
obs.setSubject(new Reference(patientId));
IIdType observationId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.setStatus(ObservationStatus.FINAL);
IIdType observationId2 = ourClient.create().resource(obs2).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().read().resourcesOfType(Observation.class).inCompartment("Patient", patientId)
.build();
}
});
Bundle bundle;
Observation response;
// Read (no masking)
response = ourClient.read().resource(Observation.class).withId(observationId).execute();
assertEquals(ObservationStatus.FINAL, response.getStatus());
assertEquals(patientId.getValue(), response.getSubject().getReference());
// Read (with _elements masking)
response = ourClient
.read()
.resource(Observation.class)
.withId(observationId)
.elementsSubset("status")
.execute();
assertEquals(ObservationStatus.FINAL, response.getStatus());
assertEquals(null, response.getSubject().getReference());
// Read a non-allowed observation
try {
ourClient.read().resource(Observation.class).withId(observationId2).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
/**
* See #751
*/
@ -472,6 +530,68 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
}
@Test
public void testTransactionResponses() {
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
// Allow write but not read
.allow("transactions").transaction().withAnyOperation().andApplyNormalRules().andThen()
.allow("write patient").write().resourcesOfType(Encounter.class).withAnyId().andThen()
.denyAll("deny all")
.build();
}
});
// Create a bundle that will be used as a transaction
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
Encounter encounter = new Encounter();
encounter.addIdentifier(new Identifier().setSystem("http://foo").setValue("123"));
encounter.setStatus(Encounter.EncounterStatus.FINISHED);
bundle.addEntry()
.setFullUrl("Encounter")
.setResource(encounter)
.getRequest()
.setUrl("Encounter")
.setMethod(Bundle.HTTPVerb.POST);
// return=minimal - should succeed
Bundle resp = ourClient
.transaction()
.withBundle(bundle)
.withAdditionalHeader(Constants.HEADER_PREFER, "return=" + Constants.HEADER_PREFER_RETURN_MINIMAL)
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
assertNull(resp.getEntry().get(0).getResource());
// return=OperationOutcome - should succeed
resp = ourClient
.transaction()
.withBundle(bundle)
.withAdditionalHeader(Constants.HEADER_PREFER, "return=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME)
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
assertNull(resp.getEntry().get(0).getResource());
// return=Representation - should fail
try {
ourClient
.transaction()
.withBundle(bundle)
.withAdditionalHeader(Constants.HEADER_PREFER, "return=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION)
.execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
/**
* See #762
*/
@ -553,9 +673,13 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
.setMethod(Bundle.HTTPVerb.POST);
Bundle resp = ourClient.transaction().withBundle(bundle).execute();
Bundle resp = ourClient
.transaction()
.withBundle(bundle)
.withAdditionalHeader(Constants.HEADER_PREFER, "return=" + Constants.HEADER_PREFER_RETURN_MINIMAL)
.execute();
assertEquals(3, resp.getEntry().size());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
}
@Test

View File

@ -12,6 +12,7 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR4;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -94,10 +95,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
if (ourServer == null) {
ourRestServer = new RestfulServer(myFhirCtx);
ourRestServer.registerProviders(myResourceProviders.createProviders());
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderR4.class);
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");

View File

@ -1,17 +1,17 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
import org.junit.After;
import org.junit.AfterClass;
@ -26,15 +26,14 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.*;
public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
@ -135,27 +134,43 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testInterceptorHookIsCalledForAllContents_RESOURCE_MAY_BE_RETURNED() throws IOException {
public void testInterceptorHookIsCalledForAllContents_STORAGE_PREACCESS_RESOURCES() throws IOException {
IAnonymousInterceptor pointcut = mock(IAnonymousInterceptor.class);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCE, pointcut);
IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.STORAGE_PREACCESS_RESOURCES, interceptor);
try {
String theUrl = ourServerBase + "/" + compId + "/$document?_format=json";
fetchBundle(theUrl, EncodingEnum.JSON);
ourLog.info("Composition ID: {}", compId);
List<String> returnedClasses = new ArrayList<>();
doAnswer(t->{
HookParams param = t.getArgument(1, HookParams.class);
IPreResourceAccessDetails nextPreResourceAccessDetails = param.get(IPreResourceAccessDetails.class);
for (int i = 0; i < nextPreResourceAccessDetails.size(); i++) {
String className = nextPreResourceAccessDetails.getResource(i).getClass().getSimpleName();
ourLog.info("* Preaccess called on {}", nextPreResourceAccessDetails.getResource(i).getIdElement().getValue());
returnedClasses.add(className);
}
return null;
}).when(interceptor).invoke(eq(Pointcut.STORAGE_PREACCESS_RESOURCES), any());
Mockito.verify(pointcut, times(10)).invoke(eq(Pointcut.STORAGE_PREACCESS_RESOURCE), myHookParamsCaptor.capture());
String theUrl = ourServerBase + "/" + compId + "/$document?_format=json";
Bundle bundle = fetchBundle(theUrl, EncodingEnum.JSON);
for (Bundle.BundleEntryComponent next : bundle.getEntry()) {
ourLog.info("Bundle contained: {}", next.getResource().getIdElement().getValue());
}
List<String> returnedClasses = myHookParamsCaptor
.getAllValues()
.stream()
.map(t -> t.get(IBaseResource.class, 0))
.map(t -> t.getClass().getSimpleName())
.collect(Collectors.toList());
Mockito.verify(interceptor, times(2)).invoke(eq(Pointcut.STORAGE_PREACCESS_RESOURCES), myHookParamsCaptor.capture());
ourLog.info("Returned classes: {}", returnedClasses);
ourLog.info("Returned classes: {}", returnedClasses);
assertThat(returnedClasses, hasItem("Composition"));
assertThat(returnedClasses, hasItem("Organization"));
assertThat(returnedClasses, hasItem("Composition"));
assertThat(returnedClasses, hasItem("Organization"));
} finally {
ourRestServer.getInterceptorService().unregisterInterceptor(interceptor);
}
}
private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException {

View File

@ -0,0 +1,759 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.interceptor.consent.*;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(ConsentInterceptorResourceProviderR4Test.class);
private List<String> myObservationIds;
private List<String> myPatientIds;
private List<String> myObservationIdsOddOnly;
private List<String> myObservationIdsEvenOnly;
private List<String> myObservationIdsEvenOnlyBackwards;
private ConsentInterceptor myConsentInterceptor;
@Autowired
@Qualifier(BaseConfig.GRAPHQL_PROVIDER_NAME)
private Object myGraphQlProvider;
@Override
@After
public void after() throws Exception {
super.after();
Validate.notNull(myConsentInterceptor);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
ourRestServer.getInterceptorService().unregisterInterceptor(myConsentInterceptor);
ourRestServer.unregisterProvider(myGraphQlProvider);
}
@Override
@Before
public void before() throws Exception {
super.before();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
ourRestServer.registerProvider(myGraphQlProvider);
}
@Test
public void testSearchAndBlockSomeWithReject() {
create50Observations();
IConsentService consentService = new ConsentSvcCantSeeOddNumbered();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Perform a search
Bundle result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, result);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(0, 15), returnedIdValues);
// Fetch the next page
result = ourClient
.loadPage()
.next(result)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(15, 25), returnedIdValues);
}
/**
* Make sure that the query cache doesn't get used at all if the consent
* service wants to inspect a request
*/
@Test
public void testSearchAndBlockSome_DontReuseSearches() {
create50Observations();
CapturingInterceptor capture = new CapturingInterceptor();
ourClient.registerInterceptor(capture);
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Perform a search and only allow even
consentService.setTarget(new ConsentSvcCantSeeOddNumbered());
Bundle result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, result);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnly.subList(0, 15), returnedIdValues);
List<String> cacheOutcome = capture.getLastResponse().getHeaders(Constants.HEADER_X_CACHE);
assertEquals(0, cacheOutcome.size());
// Perform a search and only allow odd
consentService.setTarget(new ConsentSvcCantSeeEvenNumbered());
result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsOddOnly.subList(0, 15), returnedIdValues);
cacheOutcome = capture.getLastResponse().getHeaders(Constants.HEADER_X_CACHE);
assertEquals(0, cacheOutcome.size());
// Perform a search and allow all with a PROCEED
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.PROCEED));
result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(0, 15), returnedIdValues);
cacheOutcome = capture.getLastResponse().getHeaders(Constants.HEADER_X_CACHE);
assertEquals(0, cacheOutcome.size());
// Perform a search and allow all with an AUTHORIZED (no further checking)
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.AUTHORIZED));
result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(0, 15), returnedIdValues);
cacheOutcome = capture.getLastResponse().getHeaders(Constants.HEADER_X_CACHE);
assertEquals(0, cacheOutcome.size());
// Perform a second search and allow all with an AUTHORIZED (no further checking)
// which means we should finally get one from the cache
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.AUTHORIZED));
result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIds.subList(0, 15), returnedIdValues);
cacheOutcome = capture.getLastResponse().getHeaders(Constants.HEADER_X_CACHE);
assertThat(cacheOutcome.get(0), matchesPattern("^HIT from .*"));
ourClient.unregisterInterceptor(capture);
}
@Test
public void testSearchMaskSubject() {
create50Observations();
ConsentSvcMaskObservationSubjects consentService = new ConsentSvcMaskObservationSubjects();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Perform a search
Bundle result = ourClient
.search()
.forResource("Observation")
.sort()
.ascending(Observation.SP_IDENTIFIER)
.returnBundle(Bundle.class)
.count(15)
.execute();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, result);
assertEquals(15, resources.size());
assertEquals(16, consentService.getSeeCount());
resources.forEach(t -> {
assertEquals(null, ((Observation) t).getSubject().getReference());
});
// Fetch the next page
result = ourClient
.loadPage()
.next(result)
.execute();
resources = BundleUtil.toListOfResources(myFhirCtx, result);
assertEquals(15, resources.size());
assertEquals(32, consentService.getSeeCount());
resources.forEach(t -> {
assertEquals(null, ((Observation) t).getSubject().getReference());
});
}
@Test
public void testHistoryAndBlockSome() {
create50Observations();
IConsentService consentService = new ConsentSvcCantSeeOddNumbered();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Perform a search
Bundle result = ourClient
.history()
.onServer()
.returnBundle(Bundle.class)
.count(10)
.execute();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, result);
List<String> returnedIdValues = toUnqualifiedVersionlessIdValues(resources);
assertEquals(myObservationIdsEvenOnlyBackwards.subList(0, 5), returnedIdValues);
}
@Test
public void testReadAndBlockSome() {
create50Observations();
IConsentService consentService = new ConsentSvcCantSeeOddNumbered();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
ourClient.read().resource("Observation").withId(new IdType(myObservationIdsEvenOnly.get(0))).execute();
ourClient.read().resource("Observation").withId(new IdType(myObservationIdsEvenOnly.get(1))).execute();
try {
ourClient.read().resource("Observation").withId(new IdType(myObservationIdsOddOnly.get(0))).execute();
fail();
} catch (ResourceNotFoundException e) {
// good
}
try {
ourClient.read().resource("Observation").withId(new IdType(myObservationIdsOddOnly.get(1))).execute();
fail();
} catch (ResourceNotFoundException e) {
// good
}
}
@Test
public void testCreateBlockResponse() throws IOException {
create50Observations();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
Patient patient = new Patient();
patient.setActive(true);
// Reject output
consentService.setTarget(new ConsentSvcRejectSeeingAnything());
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
post.setEntity(toEntity(patient));
try (CloseableHttpResponse status = ourHttpClient.execute(post)) {
String id = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue();
assertThat(id, matchesPattern("^.*/Patient/[0-9]+/_history/[0-9]+$"));
assertEquals(201, status.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
assertThat(responseString, blankOrNullString());
assertNull(status.getEntity().getContentType());
}
// Accept output
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.PROCEED));
post = new HttpPost(ourServerBase + "/Patient");
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
post.setEntity(toEntity(patient));
try (CloseableHttpResponse status = ourHttpClient.execute(post)) {
String id = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue();
assertThat(id, matchesPattern("^.*/Patient/[0-9]+/_history/[0-9]+$"));
assertEquals(201, status.getStatusLine().getStatusCode());
assertNotNull(status.getEntity());
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
assertThat(responseString, not(blankOrNullString()));
assertThat(status.getEntity().getContentType().getValue().toLowerCase(), matchesPattern(".*json.*"));
}
}
@Test
public void testUpdateBlockResponse() throws IOException {
create50Observations();
Patient patient = new Patient();
patient.setActive(true);
IIdType id = ourClient.create().resource(patient).prefer(PreferReturnEnum.REPRESENTATION).execute().getId().toUnqualifiedVersionless();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Reject output
consentService.setTarget(new ConsentSvcRejectSeeingAnything());
patient = new Patient();
patient.setId(id);
patient.setActive(true);
patient.addIdentifier().setValue("VAL1");
HttpPut put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart());
put.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
put.setEntity(toEntity(patient));
try (CloseableHttpResponse status = ourHttpClient.execute(put)) {
String idVal = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue();
assertThat(idVal, matchesPattern("^.*/Patient/[0-9]+/_history/[0-9]+$"));
assertEquals(200, status.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
assertThat(responseString, blankOrNullString());
assertNull(status.getEntity().getContentType());
}
// Accept output
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.PROCEED));
patient = new Patient();
patient.setId(id);
patient.setActive(true);
patient.addIdentifier().setValue("VAL2");
put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart());
put.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
put.setEntity(toEntity(patient));
try (CloseableHttpResponse status = ourHttpClient.execute(put)) {
String idVal = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue();
assertThat(idVal, matchesPattern("^.*/Patient/[0-9]+/_history/[0-9]+$"));
assertEquals(200, status.getStatusLine().getStatusCode());
assertNotNull(status.getEntity());
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
assertThat(responseString, not(blankOrNullString()));
assertThat(status.getEntity().getContentType().getValue().toLowerCase(), matchesPattern(".*json.*"));
}
}
@Test
public void testGraphQL_Proceed() throws IOException {
createPatientAndOrg();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
// Proceed everything
consentService.setTarget(new ConsentSvcNop(ConsentOperationStatusEnum.PROCEED));
String query = "{ name { family, given }, managingOrganization { reference, resource {name} } }";
String url = ourServerBase + "/" + myPatientIds.get(0) + "/$graphql?query=" + UrlUtil.escapeUrlParam(query);
ourLog.info("HTTP GET {}", url);
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_JSON);
try (CloseableHttpResponse status = ourHttpClient.execute(get)) {
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseString);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseString, containsString("\"family\":\"PATIENT_FAMILY\""));
assertThat(responseString, containsString("\"given\":[\"PATIENT_GIVEN1\",\"PATIENT_GIVEN2\"]"));
assertThat(responseString, containsString("\"name\":\"ORG_NAME\""));
}
}
@Test
public void testGraphQL_RejectResource() throws IOException {
createPatientAndOrg();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
IConsentService svc = mock(IConsentService.class);
when(svc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(svc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.REJECT);
consentService.setTarget(svc);
String query = "{ name { family, given }, managingOrganization { reference, resource {name} } }";
String url = ourServerBase + "/" + myPatientIds.get(0) + "/$graphql?query=" + UrlUtil.escapeUrlParam(query);
ourLog.info("HTTP GET {}", url);
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_JSON);
try (CloseableHttpResponse status = ourHttpClient.execute(get)) {
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseString);
assertEquals(404, status.getStatusLine().getStatusCode());
assertThat(responseString, not(containsString("\"family\":\"PATIENT_FAMILY\"")));
assertThat(responseString, not(containsString("\"given\":[\"PATIENT_GIVEN1\",\"PATIENT_GIVEN2\"]")));
assertThat(responseString, not(containsString("\"name\":\"ORG_NAME\"")));
OperationOutcome oo = myFhirCtx.newJsonParser().parseResource(OperationOutcome.class, responseString);
assertThat(oo.getIssueFirstRep().getDiagnostics(), matchesPattern("Unable to execute GraphQL Expression: HTTP 404 Resource Patient/[0-9]+ is not known"));
}
}
@Test
public void testGraphQL_RejectLinkedResource() throws IOException {
createPatientAndOrg();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
IConsentService svc = mock(IConsentService.class);
when(svc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(svc.canSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t -> {
IBaseResource resource = t.getArgument(1, IBaseResource.class);
if (resource instanceof Organization) {
return ConsentOutcome.REJECT;
}
return ConsentOutcome.PROCEED;
});
when(svc.seeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
consentService.setTarget(svc);
String query = "{ name { family, given }, managingOrganization { reference, resource {name} } }";
String url = ourServerBase + "/" + myPatientIds.get(0) + "/$graphql?query=" + UrlUtil.escapeUrlParam(query);
ourLog.info("HTTP GET {}", url);
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_JSON);
try (CloseableHttpResponse status = ourHttpClient.execute(get)) {
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseString);
assertEquals(404, status.getStatusLine().getStatusCode());
assertThat(responseString, not(containsString("\"family\":\"PATIENT_FAMILY\"")));
assertThat(responseString, not(containsString("\"given\":[\"PATIENT_GIVEN1\",\"PATIENT_GIVEN2\"]")));
assertThat(responseString, not(containsString("\"name\":\"ORG_NAME\"")));
OperationOutcome oo = myFhirCtx.newJsonParser().parseResource(OperationOutcome.class, responseString);
assertThat(oo.getIssueFirstRep().getDiagnostics(), matchesPattern("Unable to execute GraphQL Expression: HTTP 404 Resource Organization/[0-9]+ is not known"));
}
}
@Test
public void testGraphQL_MaskLinkedResource() throws IOException {
createPatientAndOrg();
DelegatingConsentService consentService = new DelegatingConsentService();
myConsentInterceptor = new ConsentInterceptor(consentService, IConsentContextServices.NULL_IMPL);
ourRestServer.getInterceptorService().registerInterceptor(myConsentInterceptor);
IConsentService svc = mock(IConsentService.class);
when(svc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(svc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(svc.seeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t -> {
IBaseResource resource = t.getArgument(1, IBaseResource.class);
if (resource instanceof Organization) {
Organization org = new Organization();
org.addIdentifier().setSystem("ORG_SYSTEM").setValue("ORG_VALUE");
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED, org);
}
return ConsentOutcome.PROCEED;
});
consentService.setTarget(svc);
String query = "{ name { family, given }, managingOrganization { reference, resource {name, identifier { system } } } }";
String url = ourServerBase + "/" + myPatientIds.get(0) + "/$graphql?query=" + UrlUtil.escapeUrlParam(query);
ourLog.info("HTTP GET {}", url);
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_JSON);
try (CloseableHttpResponse status = ourHttpClient.execute(get)) {
String responseString = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseString);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseString, containsString("\"family\":\"PATIENT_FAMILY\""));
assertThat(responseString, containsString("\"given\":[\"PATIENT_GIVEN1\",\"PATIENT_GIVEN2\"]"));
assertThat(responseString, not(containsString("\"name\":\"ORG_NAME\"")));
assertThat(responseString, containsString("\"system\":\"ORG_SYSTEM\""));
}
}
private void createPatientAndOrg() {
myPatientIds = new ArrayList<>();
Organization org = new Organization();
org.setName("ORG_NAME");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.setActive(true);
p.addName().setFamily("PATIENT_FAMILY").addGiven("PATIENT_GIVEN1").addGiven("PATIENT_GIVEN2");
p.getManagingOrganization().setReference(orgId.getValue());
String pid = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue();
myPatientIds.add(pid);
}
private void create50Observations() {
myPatientIds = new ArrayList<>();
myObservationIds = new ArrayList<>();
Patient p = new Patient();
p.setActive(true);
String pid = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue();
myPatientIds.add(pid);
for (int i = 0; i < 50; i++) {
final Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.addIdentifier().setSystem("urn:system").setValue("I" + leftPad("" + i, 5, '0'));
obs1.getSubject().setReference(pid);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
myObservationIds.add(obs1id.toUnqualifiedVersionless().getValue());
}
myObservationIdsEvenOnly =
myObservationIds
.stream()
.filter(t -> Long.parseLong(t.substring(t.indexOf('/') + 1)) % 2 == 0)
.collect(Collectors.toList());
myObservationIdsOddOnly = ListUtils.removeAll(myObservationIds, myObservationIdsEvenOnly);
myObservationIdsEvenOnlyBackwards = Lists.reverse(myObservationIdsEvenOnly);
}
private HttpEntity toEntity(Patient thePatient) {
String encoded = myFhirCtx.newJsonParser().encodeResourceToString(thePatient);
ContentType cs = ContentType.create(Constants.CT_FHIR_JSON, Constants.CHARSET_UTF8);
return new StringEntity(encoded, cs);
}
private class ConsentSvcMaskObservationSubjects implements IConsentService {
private int mySeeCount = 0;
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
int getSeeCount() {
return mySeeCount;
}
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
mySeeCount++;
String resourceId = theResource.getIdElement().toUnqualifiedVersionless().getValue();
ourLog.info("** SEE: {}", resourceId);
if (theResource instanceof Observation) {
((Observation) theResource).getSubject().setReference("");
((Observation) theResource).getSubject().setResource(null);
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED, theResource);
}
return ConsentOutcome.PROCEED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// nothing
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// nothing
}
}
private static class ConsentSvcCantSeeOddNumbered implements IConsentService {
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED);
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
Long resIdLong = theResource.getIdElement().getIdPartAsLong();
if (resIdLong % 2 == 1) {
return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
}
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED);
}
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// nothing
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// nothing
}
}
private static class ConsentSvcCantSeeEvenNumbered implements IConsentService {
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED);
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
Long resIdLong = theResource.getIdElement().getIdPartAsLong();
if (resIdLong % 2 == 0) {
return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
}
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED);
}
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// nothing
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// nothing
}
}
private static class ConsentSvcNop implements IConsentService {
private final ConsentOperationStatusEnum myOperationStatus;
private ConsentSvcNop(ConsentOperationStatusEnum theOperationStatus) {
myOperationStatus = theOperationStatus;
}
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return new ConsentOutcome(myOperationStatus);
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return new ConsentOutcome(ConsentOperationStatusEnum.PROCEED);
}
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// nothing
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// nothing
}
}
private static class ConsentSvcRejectSeeingAnything implements IConsentService {
@Override
public ConsentOutcome startOperation(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.REJECT;
}
@Override
public ConsentOutcome seeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
}
@Override
public void completeOperationSuccess(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
// nothing
}
@Override
public void completeOperationFailure(RequestDetails theRequestDetails, BaseServerResponseException theException, IConsentContextServices theContextServices) {
// nothing
}
}
}

View File

@ -1,15 +1,15 @@
package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.model.dstu2.resource.Patient;
@ -30,25 +30,19 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManager;
import java.io.IOException;
import java.util.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SuppressWarnings({"unchecked"})
@ -106,22 +100,20 @@ public class SearchCoordinatorSvcImplTest {
when(myCallingDao.newSearchBuilder()).thenReturn(mySearchBuider);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
doAnswer(theInvocation -> {
PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) theInvocation.getArguments()[0];
provider.setSearchCoordinatorSvc(mySvc);
provider.setPlatformTransactionManager(myTxManager);
provider.setSearchDao(mySearchDao);
provider.setEntityManager(myEntityManager);
provider.setContext(ourCtx);
provider.setInterceptorBroadcaster(myInterceptorBroadcaster);
return null;
}
}).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class));
}
private List<Long> createPidSequence(int from, int to) {
List<Long> pids = new ArrayList<Long>();
List<Long> pids = new ArrayList<>();
for (long i = from; i < to; i++) {
pids.add(i);
}
@ -129,20 +121,16 @@ public class SearchCoordinatorSvcImplTest {
}
private Answer<Void> loadPids() {
Answer<Void> retVal = new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
return theInvocation -> {
List<Long> pids = (List<Long>) theInvocation.getArguments()[0];
List<IBaseResource> resources = (List<IBaseResource>) theInvocation.getArguments()[1];
List<IBaseResource> resources = (List<IBaseResource>) theInvocation.getArguments()[2];
for (Long nextPid : pids) {
Patient pt = new Patient();
pt.setId(nextPid.toString());
resources.add(pt);
}
return null;
}
};
return retVal;
}
@Test
@ -173,8 +161,8 @@ public class SearchCoordinatorSvcImplTest {
List<Long> pids = createPidSequence(10, 800);
SlowIterator iter = new SlowIterator(pids.iterator(), 1);
when(mySearchBuider.createQuery(any(), any(), nullable(RequestDetails.class))).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
when(mySearchBuider.createQuery(any(), any(), any())).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
when(mySearchResultDao.findWithSearchUuid(any(), any())).thenAnswer(t -> {
List<Long> returnedValues = iter.getReturnedValues();
@ -230,9 +218,9 @@ public class SearchCoordinatorSvcImplTest {
List<Long> pids = createPidSequence(10, 800);
SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuider.createQuery(Mockito.same(params), any(), nullable(RequestDetails.class))).thenReturn(iter);
when(mySearchBuider.createQuery(same(params), any(), any())).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNotNull(result.getUuid());
@ -258,9 +246,9 @@ public class SearchCoordinatorSvcImplTest {
List<Long> pids = createPidSequence(10, 800);
IResultIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuider.createQuery(Mockito.same(params), any(), nullable(RequestDetails.class))).thenReturn(iter);
when(mySearchBuider.createQuery(same(params), any(), any())).thenReturn(iter);
when(mySearchDao.save(any())).thenAnswer(t -> t.getArguments()[0]);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNotNull(result.getUuid());
@ -302,9 +290,9 @@ public class SearchCoordinatorSvcImplTest {
List<Long> pids = createPidSequence(10, 100);
SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuider.createQuery(Mockito.same(params), any(), nullable(RequestDetails.class))).thenReturn(iter);
when(mySearchBuider.createQuery(same(params), any(), any())).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNotNull(result.getUuid());
@ -333,37 +321,31 @@ public class SearchCoordinatorSvcImplTest {
search.setResourceType("Patient");
when(mySearchDao.findByUuid(eq(uuid))).thenReturn(search);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
PersistedJpaBundleProvider provider;
List<IBaseResource> resources;
new Thread() {
@Override
public void run() {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignore
}
when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(new Answer<Page<Long>>() {
@Override
public Page<Long> answer(InvocationOnMock theInvocation) throws Throwable {
when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(theInvocation -> {
Pageable page = (Pageable) theInvocation.getArguments()[1];
ArrayList<Long> results = new ArrayList<Long>();
ArrayList<Long> results = new ArrayList<>();
int max = (page.getPageNumber() * page.getPageSize()) + page.getPageSize();
for (long i = page.getOffset(); i < max; i++) {
results.add(i + 10L);
}
return new PageImpl<Long>(results);
}
return new PageImpl<>(results);
});
search.setStatus(SearchStatusEnum.FINISHED);
}
}.start();
}).start();
/*
* Now call from a new bundle provider. This simulates a separate HTTP
@ -391,9 +373,9 @@ public class SearchCoordinatorSvcImplTest {
params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800);
when(mySearchBuider.createQuery(Mockito.same(params), any(), nullable(RequestDetails.class))).thenReturn(new ResultIterator(pids.iterator()));
when(mySearchBuider.createQuery(same(params), any(), any())).thenReturn(new ResultIterator(pids.iterator()));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNull(result.getUuid());
@ -415,7 +397,7 @@ public class SearchCoordinatorSvcImplTest {
when(mySearchBuider.createQuery(Mockito.same(params), any(), nullable(RequestDetails.class))).thenReturn(new ResultIterator(pids.iterator()));
pids = createPidSequence(10, 110);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao), nullable(RequestDetails.class));
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(Collection.class), any(List.class), anyBoolean(), nullable(RequestDetails.class));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNull(result.getUuid());
@ -432,7 +414,7 @@ public class SearchCoordinatorSvcImplTest {
private int myCount;
private IResultIterator myWrap;
public FailAfterNIterator(IResultIterator theWrap, int theCount) {
FailAfterNIterator(IResultIterator theWrap, int theCount) {
myWrap = theWrap;
myCount = theCount;
}
@ -457,7 +439,7 @@ public class SearchCoordinatorSvcImplTest {
}
@Override
public void close() throws IOException {
public void close() {
// nothing
}
}
@ -466,7 +448,7 @@ public class SearchCoordinatorSvcImplTest {
private final Iterator<Long> myWrap;
public ResultIterator(Iterator<Long> theWrap) {
ResultIterator(Iterator<Long> theWrap) {
myWrap = theWrap;
}
@ -486,7 +468,7 @@ public class SearchCoordinatorSvcImplTest {
}
@Override
public void close() throws IOException {
public void close() {
// nothing
}
}
@ -505,19 +487,13 @@ public class SearchCoordinatorSvcImplTest {
private Iterator<Long> myWrap;
private List<Long> myReturnedValues = new ArrayList<>();
public SlowIterator(Iterator<Long> theWrap, int theDelay) {
SlowIterator(Iterator<Long> theWrap, int theDelay) {
myWrap = theWrap;
myDelay = theDelay;
myResultIteratorWrap = null;
}
public SlowIterator(IResultIterator theWrap, int theDelay) {
myWrap = theWrap;
myResultIteratorWrap = theWrap;
myDelay = theDelay;
}
public List<Long> getReturnedValues() {
List<Long> getReturnedValues() {
return myReturnedValues;
}

View File

@ -139,7 +139,7 @@ public class StressTestR4Test extends BaseResourceProviderR4Test {
for (int i = 0; i <= count; i += 100) {
List<IBaseResource> resultsAndIncludes = results.getResources(i, i + 100);
ids.addAll(toUnqualifiedVersionlessIdValues(resultsAndIncludes));
results = myPagingProvider.retrieveResultList(results.getUuid(), null);
results = myPagingProvider.retrieveResultList(null, results.getUuid());
}
assertEquals(count, ids.size());
assertEquals(count, Sets.newHashSet(ids).size());
@ -152,7 +152,7 @@ public class StressTestR4Test extends BaseResourceProviderR4Test {
for (int i = 1000; i <= count; i += 100) {
List<IBaseResource> resultsAndIncludes = results.getResources(i, i + 100);
ids.addAll(toUnqualifiedVersionlessIdValues(resultsAndIncludes));
results = myPagingProvider.retrieveResultList(results.getUuid(), null);
results = myPagingProvider.retrieveResultList(null, results.getUuid());
}
assertEquals(count - 1000, ids.size());
assertEquals(count - 1000, Sets.newHashSet(ids).size());
@ -390,11 +390,8 @@ public class StressTestR4Test extends BaseResourceProviderR4Test {
}
ourClient.transaction().withBundle(input).execute();
CloseableHttpResponse getMeta = ourHttpClient.execute(new HttpGet(ourServerBase + "/metadata"));
try {
try (CloseableHttpResponse getMeta = ourHttpClient.execute(new HttpGet(ourServerBase + "/metadata"))) {
assertEquals(200, getMeta.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(getMeta);
}
List<BaseTask> tasks = Lists.newArrayList();

View File

@ -162,9 +162,10 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
}
@Test
public void testMemorytrategyMeta() throws InterruptedException {
public void testMemoryStrategyMeta() throws InterruptedException {
String inMemoryCriteria = "Observation?code=17861-6";
Subscription subscription = createSubscription(inMemoryCriteria, null, ourNotificationListenerServer);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(subscription));
List<Coding> tag = subscription.getMeta().getTag();
assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.get(0).getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.get(0).getCode());

View File

@ -1,4 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
@ -10,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.8.0-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -201,11 +202,19 @@
For some reason JavaDoc crashed during site generation unless we have this dependency
-->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<scope>provided</scope>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -97,8 +97,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
/**
* 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();
retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat(

View File

@ -5,6 +5,7 @@ import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.hl7.fhir.dstu3.model.Patient;
@ -15,7 +16,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.util.JettyUtil;
public class ExampleServerIT {

View File

@ -26,6 +26,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>

View File

@ -20,24 +20,33 @@ package ca.uhn.fhir.jpa.model.search;
* #L%
*/
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.StopWatch;
import javax.annotation.Nullable;
/**
* This class contains a runtime in-memory description of a search operation,
* including details on processing time and other things
*/
public class SearchRuntimeDetails {
private final String mySearchUuid;
private final RequestDetails myRequestDetails;
private StopWatch myQueryStopwatch;
private int myFoundMatchesCount;
private boolean myLoadSynchronous;
private String myQueryString;
private SearchStatusEnum mySearchStatus;
public SearchRuntimeDetails(String theSearchUuid) {
public SearchRuntimeDetails(RequestDetails theRequestDetails, String theSearchUuid) {
myRequestDetails = theRequestDetails;
mySearchUuid = theSearchUuid;
}
@Nullable
public RequestDetails getRequestDetails() {
return myRequestDetails;
}
public String getSearchUuid() {
return mySearchUuid;
}
@ -50,30 +59,30 @@ public class SearchRuntimeDetails {
myQueryStopwatch = theQueryStopwatch;
}
public void setFoundMatchesCount(int theFoundMatchesCount) {
myFoundMatchesCount = theFoundMatchesCount;
}
public int getFoundMatchesCount() {
return myFoundMatchesCount;
}
public void setLoadSynchronous(boolean theLoadSynchronous) {
myLoadSynchronous = theLoadSynchronous;
public void setFoundMatchesCount(int theFoundMatchesCount) {
myFoundMatchesCount = theFoundMatchesCount;
}
public boolean getLoadSynchronous() {
return myLoadSynchronous;
}
public void setQueryString(String theQueryString) {
myQueryString = theQueryString;
public void setLoadSynchronous(boolean theLoadSynchronous) {
myLoadSynchronous = theLoadSynchronous;
}
public String getQueryString() {
return myQueryString;
}
public void setQueryString(String theQueryString) {
myQueryString = theQueryString;
}
public SearchStatusEnum getSearchStatus() {
return mySearchStatus;
}

View File

@ -33,4 +33,8 @@ public class StorageProcessingMessage {
return this;
}
@Override
public String toString() {
return myMessage;
}
}

View File

@ -419,7 +419,7 @@ public class SearchParameterMap implements Serializable {
b.append(getCount());
}
// Summary
// Summary mode (_summary)
if (getSummaryMode() != null) {
addUrlParamSeparator(b);
b.append(Constants.PARAM_SUMMARY);
@ -427,6 +427,7 @@ public class SearchParameterMap implements Serializable {
b.append(getSummaryMode().getCode());
}
// Search count mode (_total)
if (getSearchTotalMode() != null) {
addUrlParamSeparator(b);
b.append(Constants.PARAM_SEARCH_TOTAL_MODE);

View File

@ -48,16 +48,16 @@ public class SearchableHashMapResourceProvider<T extends IBaseResource> extends
}
public List<T> searchByCriteria(String theCriteria, RequestDetails theRequest) {
return searchBy(resource -> mySearchParamMatcher.match(theCriteria, resource, theRequest));
return searchBy(resource -> mySearchParamMatcher.match(theCriteria, resource, theRequest), theRequest);
}
public List<T> searchByParams(SearchParameterMap theSearchParams, RequestDetails theRequest) {
return searchBy(resource -> mySearchParamMatcher.match(theSearchParams.toNormalizedQueryString(getFhirContext()), resource, theRequest));
return searchBy(resource -> mySearchParamMatcher.match(theSearchParams.toNormalizedQueryString(getFhirContext()), resource, theRequest), theRequest);
}
private List<T> searchBy(Function<IBaseResource, InMemoryMatchResult> theMatcher) {
List<T> allEResources = searchAll();
private List<T> searchBy(Function<IBaseResource, InMemoryMatchResult> theMatcher, RequestDetails theRequest) {
List<T> allEResources = searchAll(theRequest);
List<T> matches = new ArrayList<>();
for (T resource : allEResources) {
InMemoryMatchResult result = theMatcher.apply(resource);

View File

@ -32,6 +32,8 @@ import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.SearchParameterUtil;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
@ -189,8 +191,10 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
ourLog.warn(message);
// Interceptor broadcast: JPA_PERFTRACE_WARNING
HookParams params = new HookParams();
params.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
HookParams params = new HookParams()
.add(RequestDetails.class, null)
.add(ServletRequestDetails.class, null)
.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
myInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
}
}

View File

@ -9,8 +9,10 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
@ -48,6 +50,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class RequestDetails {
private final StopWatch myRequestStopwatch = new StopWatch();
private IInterceptorBroadcaster myInterceptorBroadcaster;
private String myTenantId;
private String myCompartmentName;
@ -68,6 +71,7 @@ public abstract class RequestDetails {
private boolean mySubRequest;
private Map<String, List<String>> myUnqualifiedToQualifiedNames;
private Map<Object, Object> myUserData;
private IBaseResource myResource;
/**
* Constructor
@ -76,6 +80,32 @@ public abstract class RequestDetails {
myInterceptorBroadcaster = theInterceptorBroadcaster;
}
public StopWatch getRequestStopwatch() {
return myRequestStopwatch;
}
/**
* Returns the request resource (as provided in the request body) if it has been parsed.
* Note that this value is only set fairly late in the processing pipeline, so it
* may not always be set, even for operations that take a resource as input.
*
* @since 4.0.0
*/
public IBaseResource getResource() {
return myResource;
}
/**
* Sets the request resource (as provided in the request body) if it has been parsed.
* Note that this value is only set fairly late in the processing pipeline, so it
* may not always be set, even for operations that take a resource as input.
*
* @since 4.0.0
*/
public void setResource(IBaseResource theResource) {
myResource = theResource;
}
public void addParameter(String theName, String[] theValues) {
getParameters();
myParameters.put(theName, theValues);
@ -487,6 +517,11 @@ public abstract class RequestDetails {
return null;
}
@Override
public boolean hasHooks(Pointcut thePointcut) {
return myWrap.hasHooks(thePointcut);
}
}

View File

@ -20,13 +20,12 @@ package ca.uhn.fhir.rest.server;
* #L%
*/
import java.util.LinkedHashMap;
import java.util.UUID;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import java.util.LinkedHashMap;
import java.util.UUID;
public class FifoMemoryPagingProvider extends BasePagingProvider implements IPagingProvider {
@ -41,12 +40,12 @@ public class FifoMemoryPagingProvider extends BasePagingProvider implements IPag
}
@Override
public synchronized IBundleProvider retrieveResultList(String theId, RequestDetails theRequest) {
public synchronized IBundleProvider retrieveResultList(RequestDetails theRequest, String theId) {
return myBundleProviders.get(theId);
}
@Override
public synchronized String storeResultList(IBundleProvider theList) {
public synchronized String storeResultList(RequestDetails theRequestDetails, IBundleProvider theList) {
while (myBundleProviders.size() > mySize) {
myBundleProviders.remove(myBundleProviders.keySet().iterator().next());
}

View File

@ -3,6 +3,9 @@ package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/*
* #%L
* HAPI FHIR - Server Framework
@ -37,7 +40,7 @@ public interface IPagingProvider {
* add this parameter and not use it if needed.
* </p>
*/
IBundleProvider retrieveResultList(String theSearchId, RequestDetails theRequest);
IBundleProvider retrieveResultList(@Nullable RequestDetails theRequestDetails, @Nonnull String theSearchId);
/**
* Retrieve a result list by ID
@ -47,13 +50,15 @@ public interface IPagingProvider {
* add this parameter and not use it if needed.
* </p>
*/
default IBundleProvider retrieveResultList(String theSearchId, String thePageId, RequestDetails theRequest) {
default IBundleProvider retrieveResultList(@Nullable RequestDetails theRequestDetails, @Nonnull String theSearchId, String thePageId) {
return null;
}
/**
* Stores a result list and returns an ID with which that list can be returned
*
* @param theRequestDetails The server request being made (may be null)
*/
String storeResultList(IBundleProvider theList);
String storeResultList(@Nullable RequestDetails theRequestDetails, IBundleProvider theList);
}

View File

@ -110,7 +110,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
private IInterceptorService myInterceptorService;
private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES;
private boolean myDefaultPrettyPrint = false;
private EncodingEnum myDefaultResponseEncoding = EncodingEnum.XML;
private EncodingEnum myDefaultResponseEncoding = EncodingEnum.JSON;
private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT;
private FhirContext myFhirContext;
private boolean myIgnoreServerParsedRequestParameters = true;
@ -965,7 +965,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
BaseMethodBinding<?> resourceMethod = determineResourceMethod(requestDetails, requestPath);
requestDetails.setRestOperationType(resourceMethod.getRestOperationType());
RestOperationTypeEnum operation = resourceMethod.getRestOperationType(requestDetails);
requestDetails.setRestOperationType(operation);
// Handle server interceptors
HookParams postProcessedParams = new HookParams();

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -664,36 +665,48 @@ public class RestfulServerUtils {
return retVal;
}
public static PreferReturnEnum parsePreferHeader(String theValue) {
if (isBlank(theValue)) {
return null;
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperationType) {
return ourOperationsWhichAllowPreferHeader.contains(theRestOperationType);
}
/**
* @param theServer If null, no default will be used. If not null, the default will be read from the server.
*/
public static PreferReturnEnum parsePreferHeader(IRestfulServer<?> theServer, String theValue) {
PreferReturnEnum retVal = null;
if (isNotBlank(theValue)) {
StringTokenizer tok = new StringTokenizer(theValue, ",");
while (tok.hasMoreTokens()) {
String next = tok.nextToken();
int eqIndex = next.indexOf('=');
if (eqIndex == -1 || eqIndex >= next.length() - 2) {
continue;
}
String key = next.substring(0, eqIndex).trim();
if (key.equals(Constants.HEADER_PREFER_RETURN) == false) {
continue;
}
String value = next.substring(eqIndex + 1).trim();
if (value.length() < 2) {
continue;
}
if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
value = value.substring(1, value.length() - 1);
}
retVal = PreferReturnEnum.fromHeaderValue(value);
}
}
StringTokenizer tok = new StringTokenizer(theValue, ",");
while (tok.hasMoreTokens()) {
String next = tok.nextToken();
int eqIndex = next.indexOf('=');
if (eqIndex == -1 || eqIndex >= next.length() - 2) {
continue;
}
String key = next.substring(0, eqIndex).trim();
if (key.equals(Constants.HEADER_PREFER_RETURN) == false) {
continue;
}
String value = next.substring(eqIndex + 1).trim();
if (value.length() < 2) {
continue;
}
if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
value = value.substring(1, value.length() - 1);
}
return PreferReturnEnum.fromHeaderValue(value);
if (retVal == null && theServer != null) {
retVal = theServer.getDefaultPreferReturn();
}
return null;
return retVal;
}
public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {

View File

@ -291,6 +291,11 @@ public interface IServerInterceptor {
@Hook(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
void processingCompletedNormally(ServletRequestDetails theRequestDetails);
/**
* @deprecated This class doesn't bring anything that can't be done with {@link RequestDetails}. That
* class should be used instead. Deprecated in 4.0.0
*/
@Deprecated
class ActionRequestDetails {
private final FhirContext myContext;
private final IIdType myId;
@ -428,13 +433,21 @@ public interface IServerInterceptor {
return;
}
IIdType previousRequestId = requestDetails.getId();
requestDetails.setId(getId());
IInterceptorService interceptorService = server.getInterceptorService();
HookParams params = new HookParams();
params.add(RestOperationTypeEnum.class, theOperationType);
params.add(this);
params.add(RequestDetails.class, this.getRequestDetails());
params.addIfMatchesType(ServletRequestDetails.class, this.getRequestDetails());
interceptorService.callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, params);
// Reset the request ID
requestDetails.setId(previousRequestId);
}
}

View File

@ -116,7 +116,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* </tr>
* <tr>
* <td>${processingTimeMillis}</td>
* <td>The number of milliseconds spent processing this request</td>
* <td>The number of milliseconds spen processing this request</td>
* </tr>
* </table>
*/

View File

@ -24,14 +24,11 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -43,9 +40,8 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -64,8 +60,11 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
@Interceptor
public class AuthorizationInterceptor implements IRuleApplier {
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
private final String myRequestSeenResourcesKey = AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
private final String myRequestRuleListKey = AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST";
private PolicyEnum myDefaultPolicy = PolicyEnum.DENY;
private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet();
@ -100,7 +99,12 @@ public class AuthorizationInterceptor implements IRuleApplier {
@Override
public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId,
IBaseResource theOutputResource) {
List<IAuthRule> rules = buildRuleList(theRequestDetails);
@SuppressWarnings("unchecked")
List<IAuthRule> rules = (List<IAuthRule>) theRequestDetails.getUserData().get(myRequestRuleListKey);
if (rules == null) {
rules = buildRuleList(theRequestDetails);
theRequestDetails.getUserData().put(myRequestRuleListKey, rules);
}
Set<AuthorizationFlagsEnum> flags = getFlags();
ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation);
@ -286,39 +290,56 @@ public class AuthorizationInterceptor implements IRuleApplier {
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, IServerInterceptor.ActionRequestDetails theProcessedRequest) {
public void incomingRequestPreHandled(RequestDetails theRequest) {
IBaseResource inputResource = null;
IIdType inputResourceId = null;
switch (determineOperationDirection(theOperation, theProcessedRequest.getResource())) {
switch (determineOperationDirection(theRequest.getRestOperationType(), theRequest.getResource())) {
case IN:
case BOTH:
inputResource = theProcessedRequest.getResource();
inputResourceId = theProcessedRequest.getId();
inputResource = theRequest.getResource();
inputResourceId = theRequest.getId();
break;
case OUT:
// inputResource = null;
inputResourceId = theProcessedRequest.getId();
inputResourceId = theRequest.getId();
break;
case NONE:
return;
}
RequestDetails requestDetails = theProcessedRequest.getRequestDetails();
applyRulesAndFailIfDeny(theOperation, requestDetails, inputResource, inputResourceId, null);
applyRulesAndFailIfDeny(theRequest.getRestOperationType(), theRequest, inputResource, inputResourceId, null);
}
@Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
public void hookPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails theDetails) {
for (int i = 0; i < theDetails.size(); i++) {
IBaseResource next = theDetails.getResource(i);
checkOutgoingResourceAndFailIfDeny(theRequestDetails, next);
}
}
@Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
public void hookOutgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
checkOutgoingResourceAndFailIfDeny(theRequestDetails, theResponseObject);
}
private void checkOutgoingResourceAndFailIfDeny(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
switch (determineOperationDirection(theRequestDetails.getRestOperationType(), null)) {
case IN:
case NONE:
return true;
return;
case BOTH:
case OUT:
break;
}
// Don't check the value twice
IdentityHashMap<IBaseResource, Boolean> alreadySeenMap = ConsentInterceptor.getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) {
return;
}
FhirContext fhirContext = theRequestDetails.getServer().getFhirContext();
List<IBaseResource> resources = Collections.emptyList();
@ -349,22 +370,20 @@ public class AuthorizationInterceptor implements IRuleApplier {
for (IBaseResource nextResponse : resources) {
applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse);
}
return true;
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
public void hookResourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
handleUserOperation(theRequest, theResource, RestOperationTypeEnum.CREATE);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED)
public void resourcePreDelete(RequestDetails theRequest, IBaseResource theResource) {
public void hookResourcePreDelete(RequestDetails theRequest, IBaseResource theResource) {
handleUserOperation(theRequest, theResource, RestOperationTypeEnum.DELETE);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
public void hookResourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
if (theOldResource != null) {
handleUserOperation(theRequest, theOldResource, RestOperationTypeEnum.UPDATE);
}

View File

@ -0,0 +1,304 @@
package ca.uhn.fhir.rest.server.interceptor.consent;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.IModelVisitor2;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Interceptor
public class ConsentInterceptor {
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
private final String myRequestAuthorizedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_AUTHORIZED";
private final String myRequestCompletedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED";
private final String myRequestSeenResourcesKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
private IConsentService myConsentService;
private IConsentContextServices myContextConsentServices;
/**
* Constructor
*/
public ConsentInterceptor() {
super();
}
/**
* Constructor
*
* @param theConsentService Must not be <code>null</code>
*/
public ConsentInterceptor(IConsentService theConsentService) {
this(theConsentService, IConsentContextServices.NULL_IMPL);
}
/**
* Constructor
*
* @param theConsentService Must not be <code>null</code>
* @param theContextConsentServices Must not be <code>null</code>
*/
public ConsentInterceptor(IConsentService theConsentService, IConsentContextServices theContextConsentServices) {
setConsentService(theConsentService);
setContextConsentServices(theContextConsentServices);
}
public void setContextConsentServices(IConsentContextServices theContextConsentServices) {
Validate.notNull(theContextConsentServices, "theContextConsentServices must not be null");
myContextConsentServices = theContextConsentServices;
}
public void setConsentService(IConsentService theConsentService) {
Validate.notNull(theConsentService, "theConsentService must not be null");
myConsentService = theConsentService;
}
@Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void interceptPreHandled(RequestDetails theRequestDetails) {
ConsentOutcome outcome = myConsentService.startOperation(theRequestDetails, myContextConsentServices);
Validate.notNull(outcome, "Consent service returned null outcome");
switch (outcome.getStatus()) {
case REJECT:
throw toForbiddenOperationException(outcome);
case PROCEED:
break;
case AUTHORIZED:
Map<Object, Object> userData = theRequestDetails.getUserData();
userData.put(myRequestAuthorizedKey, Boolean.TRUE);
break;
}
}
@Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH)
public boolean interceptPreCheckForCachedSearch(RequestDetails theRequestDetails) {
if (isRequestAuthorized(theRequestDetails)) {
return true;
}
return false;
}
@Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED)
public void interceptPreSearchRegistered(RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) {
if (!isRequestAuthorized(theRequestDetails)) {
theCachedSearchDetails.setCannotBeReused();
}
}
@Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES)
public void interceptPreAccess(RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) {
if (isRequestAuthorized(theRequestDetails)) {
return;
}
for (int i = 0; i < thePreResourceAccessDetails.size(); i++) {
IBaseResource nextResource = thePreResourceAccessDetails.getResource(i);
ConsentOutcome nextOutcome = myConsentService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
switch (nextOutcome.getStatus()) {
case PROCEED:
break;
case AUTHORIZED:
break;
case REJECT:
thePreResourceAccessDetails.setDontReturnResourceAtIndex(i);
break;
}
}
}
@Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES)
public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) {
if (isRequestAuthorized(theRequestDetails)) {
return;
}
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
IBaseResource nextResource = thePreResourceShowDetails.getResource(i);
if (alreadySeenResources.putIfAbsent(nextResource, Boolean.TRUE) != null) {
continue;
}
ConsentOutcome nextOutcome = myConsentService.seeResource(theRequestDetails, nextResource, myContextConsentServices);
switch (nextOutcome.getStatus()) {
case PROCEED:
if (nextOutcome.getResource() != null) {
thePreResourceShowDetails.setResource(i, nextOutcome.getResource());
}
break;
case AUTHORIZED:
break;
case REJECT:
if (nextOutcome.getResource() != null) {
IBaseResource newResource = nextOutcome.getResource();
thePreResourceShowDetails.setResource(i, newResource);
alreadySeenResources.put(newResource, true);
} else if (nextOutcome.getOperationOutcome() != null) {
IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
thePreResourceShowDetails.setResource(i, newOperationOutcome);
alreadySeenResources.put(newOperationOutcome, true);
} else {
String resourceId = nextResource.getIdElement().getValue();
theRequestDetails.getFhirContext().newTerser().clear(nextResource);
nextResource.setId(resourceId);
}
break;
}
}
}
private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) {
return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
}
@Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) {
if (theResource.getResponseResource() == null) {
return;
}
if (isRequestAuthorized(theRequestDetails)) {
return;
}
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
// See outer resource
if (alreadySeenResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
final ConsentOutcome outcome = myConsentService.seeResource(theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
if (outcome.getResource() != null) {
theResource.setResponseResource(outcome.getResource());
}
switch (outcome.getStatus()) {
case REJECT:
if (outcome.getOperationOutcome() != null) {
theResource.setResponseResource(outcome.getOperationOutcome());
} else {
theResource.setResponseResource(null);
theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
}
return;
case AUTHORIZED:
// Don't check children
return;
case PROCEED:
// Check children
break;
}
}
// See child resources
IBaseResource outerResource = theResource.getResponseResource();
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
IModelVisitor2 visitor = new IModelVisitor2() {
@Override
public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
// Clear the total
if (theElement instanceof IBaseBundle) {
BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null);
}
if (theElement == outerResource) {
return true;
}
if (theElement instanceof IBaseResource) {
if (alreadySeenResources.putIfAbsent((IBaseResource) theElement, Boolean.TRUE) != null) {
return true;
}
ConsentOutcome childOutcome = myConsentService.seeResource(theRequestDetails, (IBaseResource) theElement, myContextConsentServices);
IBaseResource replacementResource = null;
boolean shouldReplaceResource = false;
boolean shouldCheckChildren = false;
switch (childOutcome.getStatus()) {
case REJECT:
replacementResource = childOutcome.getOperationOutcome();
shouldReplaceResource = true;
break;
case PROCEED:
case AUTHORIZED:
replacementResource = childOutcome.getResource();
shouldReplaceResource = replacementResource != null;
shouldCheckChildren = childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
break;
}
if (shouldReplaceResource) {
IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
BaseRuntimeChildDefinition containerChildElement = theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
containerChildElement.getMutator().setValue(container, replacementResource);
}
return shouldCheckChildren;
}
return true;
}
@Override
public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
return true;
}
};
ctx.newTerser().visit(outerResource, visitor);
}
@Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
myConsentService.completeOperationFailure(theRequest, theException, myContextConsentServices);
}
@Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
public void requestSucceeded(RequestDetails theRequest) {
if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) {
return;
}
myConsentService.completeOperationSuccess(theRequest, myContextConsentServices);
}
private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
return Boolean.TRUE.equals(authorizedObj);
}
@SuppressWarnings("unchecked")
public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails, String theKey) {
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>) theRequestDetails.getUserData().get(theKey);
if (alreadySeenResources == null) {
alreadySeenResources = new IdentityHashMap<>();
theRequestDetails.getUserData().put(theKey, alreadySeenResources);
}
return alreadySeenResources;
}
private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) {
IBaseOperationOutcome operationOutcome = null;
if (theOutcome.getOperationOutcome() != null) {
operationOutcome = theOutcome.getOperationOutcome();
}
return new ForbiddenOperationException("Rejected by consent service", operationOutcome);
}
}

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