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:
parent
2bfbea4e6b
commit
10d969c514
|
@ -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
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -68,4 +68,8 @@ public class ErrorHandlerAdapter implements IParserErrorHandler {
|
|||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extensionContainsValueAndNestedExtensions(IParseLocation theLoc) {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package ca.uhn.fhir.jpa.interceptor;
|
||||
|
||||
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
|
||||
|
||||
public class JpaConsentContextServices implements IConsentContextServices {
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<>());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -33,4 +33,8 @@ public class StorageProcessingMessage {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return myMessage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue