From 78fd13c2f4363dc9ecb54b50e771e9ea0b773751 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 31 Aug 2017 08:57:51 -0400 Subject: [PATCH] Add config item to auto create references in JPA --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 15 +- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 1013 +++++++++-------- ...irResourceDaoCreatePlaceholdersR4Test.java | 140 +++ src/changes/changes.xml | 5 + 4 files changed, 683 insertions(+), 490 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index dc0b30a4b6b..28686bbadb1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -332,7 +332,15 @@ public abstract class BaseHapiFhirDao implements IDao { if (myConfig.isEnforceReferentialIntegrityOnWrite() == false) { continue; } - String resName = getContext().getResourceDefinition(type).getName(); + RuntimeResourceDefinition missingResourceDef = getContext().getResourceDefinition(type); + String resName = missingResourceDef.getName(); + + if (getConfig().isAutoCreatePlaceholderReferenceTargets()) { + IBaseResource newResource = missingResourceDef.newInstance(); + newResource.setId(resName + "/" + id); + autoCreateResource(newResource); + } + throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); } ResourceTable target = myEntityManager.find(ResourceTable.class, valueOf); @@ -367,6 +375,11 @@ public abstract class BaseHapiFhirDao implements IDao { return retVal; } + private void autoCreateResource(T theResource) { + IFhirResourceDao dao = (IFhirResourceDao) getDao(theResource.getClass()); + dao.create(theResource); + } + protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 7a0f7257ab6..734a7f92c8e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -1,16 +1,13 @@ package ca.uhn.fhir.jpa.dao; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - +import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; +import java.util.*; + /* * #%L * HAPI FHIR JPA Server @@ -31,9 +28,6 @@ import org.apache.commons.lang3.time.DateUtils; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; - public class DaoConfig { /** @@ -46,24 +40,21 @@ public class DaoConfig { * */ public static final Set DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet(Arrays.asList( - "http://hl7.org/fhir/ValueSet/*", - "http://hl7.org/fhir/CodeSystem/*", - "http://hl7.org/fhir/valueset-*", - "http://hl7.org/fhir/codesystem-*", - "http://hl7.org/fhir/StructureDefinition/*"))); - + "http://hl7.org/fhir/ValueSet/*", + "http://hl7.org/fhir/CodeSystem/*", + "http://hl7.org/fhir/valueset-*", + "http://hl7.org/fhir/codesystem-*", + "http://hl7.org/fhir/StructureDefinition/*"))); + /** + * Default value for {@link #setReuseCachedSearchResultsForMillis(Long)}: 60000ms (one minute) + */ + public static final Long DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS = DateUtils.MILLIS_PER_MINUTE; /** * Default value for {@link #setMaximumSearchResultCountInTransaction(Integer)} * * @see #setMaximumSearchResultCountInTransaction(Integer) */ private static final Integer DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = null; - - /** - * Default value for {@link #setReuseCachedSearchResultsForMillis(Long)}: 60000ms (one minute) - */ - public static final Long DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS = DateUtils.MILLIS_PER_MINUTE; - /** * update setter javadoc if default changes */ @@ -127,9 +118,11 @@ public class DaoConfig { private boolean mySuppressUpdatesWithNoChange = true; private Set myTreatBaseUrlsAsLocal = new HashSet(); private Set myTreatReferencesAsLogical = new HashSet(DEFAULT_LOGICAL_BASE_URLS); + private boolean myAutoCreatePlaceholderReferenceTargets; + /** * Add a value to the {@link #setTreatReferencesAsLogical(Set) logical references list}. - * + * * @see #setTreatReferencesAsLogical(Set) */ public void addTreatReferencesAsLogical(String theTreatReferencesAsLogical) { @@ -140,6 +133,7 @@ public class DaoConfig { } myTreatReferencesAsLogical.add(theTreatReferencesAsLogical); } + /** * When a code system is added that contains more than this number of codes, * the code system will be indexed later in an incremental process in order to @@ -152,6 +146,18 @@ public class DaoConfig { return myDeferIndexingForCodesystemsOfSize; } + /** + * When a code system is added that contains more than this number of codes, + * the code system will be indexed later in an incremental process in order to + * avoid overwhelming Lucene with a huge number of codes in a single operation. + *

+ * Defaults to 2000 + *

+ */ + public void setDeferIndexingForCodesystemsOfSize(int theDeferIndexingForCodesystemsOfSize) { + myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize; + } + /** * Unlike with normal search queries, $everything queries have their _includes loaded by the main search thread and these included results * are added to the normal search results instead of being added on as extras in a page. This means that they will not appear multiple times @@ -168,412 +174,6 @@ public class DaoConfig { return myEverythingIncludesFetchPageSize; } - /** - * Sets the number of milliseconds that search results for a given client search - * should be preserved before being purged from the database. - *

- * Search results are stored in the database so that they can be paged over multiple - * requests. After this - * number of milliseconds, they will be deleted from the database, and any paging links - * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. - *

- *

- *

- * To disable this feature entirely, see {@link #setExpireSearchResults(boolean)} - *

- * - * @since 1.5 - */ - public long getExpireSearchResultsAfterMillis() { - return myExpireSearchResultsAfterMillis; - } - - /** - * Gets the default maximum number of results to load in a query. - *

- * For example, if the database has a million Patient resources in it, and - * the client requests GET /Patient, if this value is set - * to a non-null value (default is null) only this number - * of results will be fetched. Setting this value appropriately - * can be useful to improve performance in some situations. - *

- */ - public Integer getFetchSizeDefaultMaximum() { - return myFetchSizeDefaultMaximum; - } - - /** - * Gets the maximum number of results to return in a GetTags query (DSTU1 only) - */ - public int getHardTagListLimit() { - return myHardTagListLimit; - } - - public int getIncludeLimit() { - return myIncludeLimit; - } - - /** - * Returns the interceptors which will be notified of operations. - * - * @see #setInterceptors(List) - */ - public List getInterceptors() { - if (myInterceptors == null) { - myInterceptors = new ArrayList<>(); - } - return myInterceptors; - } - - /** - * See {@link #setMaximumExpansionSize(int)} - */ - public int getMaximumExpansionSize() { - return myMaximumExpansionSize; - } - - /** - * Provides the maximum number of results which may be returned by a search (HTTP GET) which - * is executed as a sub-operation within within a FHIR transaction or - * batch operation. For example, if this value is set to 100 and - * a FHIR transaction is processed with a sub-request for Patient?gender=male, - * the server will throw an error (and the transaction will fail) if there are more than - * 100 resources on the server which match this query. - *

- * The default value is null, which means that there is no limit. - *

- */ - public Integer getMaximumSearchResultCountInTransaction() { - return myMaximumSearchResultCountInTransaction; - } - - public ResourceEncodingEnum getResourceEncoding() { - return myResourceEncoding; - } - - /** - * If set, an individual resource will not be allowed to have more than the - * given number of tags, profiles, and security labels (the limit is for the combined - * total for all of these things on an individual resource). - *

- * If set to null, no limit will be applied. - *

- *

- * The default value for this setting is 1000. - *

- */ - public Integer getResourceMetaCountHardLimit() { - return myResourceMetaCountHardLimit; - } - - /** - * If set to a non {@literal null} value (default is {@link #DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS non null}) - * if an identical search is requested multiple times within this window, the same results will be returned - * to multiple queries. For example, if this value is set to 1 minute and a client searches for all - * patients named "smith", and then a second client also performs the same search within 1 minute, - * the same cached results will be returned. - *

- * This approach can improve performance, especially under heavy load, but can also mean that - * searches may potentially return slightly out-of-date results. - *

- */ - public Long getReuseCachedSearchResultsForMillis() { - return myReuseCachedSearchResultsForMillis; - } - - public long getSubscriptionPollDelay() { - return mySubscriptionPollDelay; - } - - public Long getSubscriptionPurgeInactiveAfterMillis() { - return mySubscriptionPurgeInactiveAfterMillis; - } - - /** - * This setting may be used to advise the server that any references found in - * resources that have any of the base URLs given here will be replaced with - * simple local references. - *

- * For example, if the set contains the value http://example.com/base/ - * and a resource is submitted to the server that contains a reference to - * http://example.com/base/Patient/1, the server will automatically - * convert this reference to Patient/1 - *

- *

- * Note that this property has different behaviour from {@link DaoConfig#getTreatReferencesAsLogical()} - *

- * - * @see #getTreatReferencesAsLogical() - */ - public Set getTreatBaseUrlsAsLocal() { - return myTreatBaseUrlsAsLocal; - } - - /** - * This setting may be used to advise the server that any references found in - * resources that have any of the base URLs given here will be treated as logical - * references instead of being treated as real references. - *

- * A logical reference is a reference which is treated as an identifier, and - * does not neccesarily resolve. See {@link "http://hl7.org/fhir/references.html"} for - * a description of logical references. For example, the valueset - * {@link "http://hl7.org/fhir/valueset-quantity-comparator.html"} is a logical - * reference. - *

- *

- * Values for this field may take either of the following forms: - *

- *
    - *
  • http://example.com/some-url (will be matched exactly)
  • - *
  • http://example.com/some-base* (will match anything beginning with the part before the *)
  • - *
- * - * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property - */ - public Set getTreatReferencesAsLogical() { - return myTreatReferencesAsLogical; - } - - /** - * If set to true (default is false) the server will allow - * resources to have references to external servers. For example if this server is - * running at http://example.com/fhir and this setting is set to - * true the server will allow a Patient resource to be saved with a - * Patient.organization value of http://foo.com/Organization/1. - *

- * Under the default behaviour if this value has not been changed, the above - * resource would be rejected by the server because it requires all references - * to be resolvable on the local server. - *

- *

- * Note that external references will be indexed by the server and may be searched - * (e.g. Patient:organization), but - * chained searches (e.g. Patient:organization.name) will not work across - * these references. - *

- *

- * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value - * is set to true - *

- * - * @see #setTreatBaseUrlsAsLocal(Set) - * @see #setAllowExternalReferences(boolean) - */ - public boolean isAllowExternalReferences() { - return myAllowExternalReferences; - } - - /** - * @see #setAllowInlineMatchUrlReferences(boolean) - */ - public boolean isAllowInlineMatchUrlReferences() { - return myAllowInlineMatchUrlReferences; - } - - public boolean isAllowMultipleDelete() { - return myAllowMultipleDelete; - } - - /** - * If set to {@code true} the default search params (i.e. the search parameters that are - * defined by the FHIR specification itself) may be overridden by uploading search - * parameters to the server with the same code as the built-in search parameter. - *

- * This can be useful if you want to be able to disable or alter - * the behaviour of the default search parameters. - *

- *

- * The default value for this setting is {@code false} - *

- */ - public boolean isDefaultSearchParamsCanBeOverridden() { - return myDefaultSearchParamsCanBeOverridden; - } - - /** - * If set to false (default is true) resources will be permitted to be - * deleted even if other resources currently contain references to them. - *

- * This property can cause confusing results for clients of the server since searches, includes, - * and other FHIR features may not behave as expected when referential integrity is not - * preserved. Use this feature with caution. - *

- */ - public boolean isEnforceReferentialIntegrityOnDelete() { - return myEnforceReferentialIntegrityOnDelete; - } - - /** - * If set to false (default is true) resources will be permitted to be - * created or updated even if they contain references to local resources that do not exist. - *

- * For example, if a patient contains a reference to managing organization Organization/FOO - * but FOO is not a valid ID for an organization on the server, the operation will be blocked unless - * this propery has been set to false - *

- *

- * This property can cause confusing results for clients of the server since searches, includes, - * and other FHIR features may not behave as expected when referential integrity is not - * preserved. Use this feature with caution. - *

- */ - public boolean isEnforceReferentialIntegrityOnWrite() { - return myEnforceReferentialIntegrityOnWrite; - } - - /** - * If this is set to false (default is true) the stale search deletion - * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. - *

- * This feature is useful if you want to define your own process for deleting these (e.g. because - * you are running in a cluster) - *

- */ - public boolean isExpireSearchResults() { - return myDeleteStaleSearches; - } - - /** - * Should contained IDs be indexed the same way that non-contained IDs are (default is - * true) - */ - public boolean isIndexContainedResources() { - return myIndexContainedResources; - } - - public boolean isSchedulingDisabled() { - return mySchedulingDisabled; - } - - /** - * See {@link #setSubscriptionEnabled(boolean)} - */ - public boolean isSubscriptionEnabled() { - return mySubscriptionEnabled; - } - - /** - * If set to {@literal true} (default is true), if a client performs an update which does not actually - * result in any chance to a given resource (e.g. an update where the resource body matches the - * existing resource body in the database) the operation will succeed but a new version (and corresponding history - * entry) will not actually be created. The existing resource version will be returned to the client. - *

- * If set to {@literal false}, all updates will result in the creation of a new version - *

- */ - public boolean isSuppressUpdatesWithNoChange() { - return mySuppressUpdatesWithNoChange; - } - - /** - * If set to true (default is false) the server will allow - * resources to have references to external servers. For example if this server is - * running at http://example.com/fhir and this setting is set to - * true the server will allow a Patient resource to be saved with a - * Patient.organization value of http://foo.com/Organization/1. - *

- * Under the default behaviour if this value has not been changed, the above - * resource would be rejected by the server because it requires all references - * to be resolvable on the local server. - *

- *

- * Note that external references will be indexed by the server and may be searched - * (e.g. Patient:organization), but - * chained searches (e.g. Patient:organization.name) will not work across - * these references. - *

- *

- * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value - * is set to true - *

- * - * @see #setTreatBaseUrlsAsLocal(Set) - * @see #setAllowExternalReferences(boolean) - */ - public void setAllowExternalReferences(boolean theAllowExternalReferences) { - myAllowExternalReferences = theAllowExternalReferences; - } - - /** - * Should references containing match URLs be resolved and replaced in create and update operations. For - * example, if this property is set to true and a resource is created containing a reference - * to "Patient?identifier=12345", this is reference match URL will be resolved and replaced according - * to the usual match URL rules. - *

- * Default is {@literal true} beginning in HAPI FHIR 2.4, since this - * feature is now specified in the FHIR specification. (Previously it - * was an experimental/rpposed feature) - *

- * - * @since 1.5 - */ - public void setAllowInlineMatchUrlReferences(boolean theAllowInlineMatchUrlReferences) { - myAllowInlineMatchUrlReferences = theAllowInlineMatchUrlReferences; - } - - public void setAllowMultipleDelete(boolean theAllowMultipleDelete) { - myAllowMultipleDelete = theAllowMultipleDelete; - } - - /** - * If set to {@code true} the default search params (i.e. the search parameters that are - * defined by the FHIR specification itself) may be overridden by uploading search - * parameters to the server with the same code as the built-in search parameter. - *

- * This can be useful if you want to be able to disable or alter - * the behaviour of the default search parameters. - *

- *

- * The default value for this setting is {@code false} - *

- */ - public void setDefaultSearchParamsCanBeOverridden(boolean theDefaultSearchParamsCanBeOverridden) { - myDefaultSearchParamsCanBeOverridden = theDefaultSearchParamsCanBeOverridden; - } - - /** - * When a code system is added that contains more than this number of codes, - * the code system will be indexed later in an incremental process in order to - * avoid overwhelming Lucene with a huge number of codes in a single operation. - *

- * Defaults to 2000 - *

- */ - public void setDeferIndexingForCodesystemsOfSize(int theDeferIndexingForCodesystemsOfSize) { - myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize; - } - - /** - * If set to false (default is true) resources will be permitted to be - * deleted even if other resources currently contain references to them. - *

- * This property can cause confusing results for clients of the server since searches, includes, - * and other FHIR features may not behave as expected when referential integrity is not - * preserved. Use this feature with caution. - *

- */ - public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) { - myEnforceReferentialIntegrityOnDelete = theEnforceReferentialIntegrityOnDelete; - } - - /** - * If set to false (default is true) resources will be permitted to be - * created or updated even if they contain references to local resources that do not exist. - *

- * For example, if a patient contains a reference to managing organization Organization/FOO - * but FOO is not a valid ID for an organization on the server, the operation will be blocked unless - * this propery has been set to false - *

- *

- * This property can cause confusing results for clients of the server since searches, includes, - * and other FHIR features may not behave as expected when referential integrity is not - * preserved. Use this feature with caution. - *

- */ - public void setEnforceReferentialIntegrityOnWrite(boolean theEnforceReferentialIntegrityOnWrite) { - myEnforceReferentialIntegrityOnWrite = theEnforceReferentialIntegrityOnWrite; - } - /** * Unlike with normal search queries, $everything queries have their _includes loaded by the main search thread and these included results * are added to the normal search results instead of being added on as extras in a page. This means that they will not appear multiple times @@ -592,15 +192,23 @@ public class DaoConfig { } /** - * If this is set to false (default is true) the stale search deletion - * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. + * Sets the number of milliseconds that search results for a given client search + * should be preserved before being purged from the database. *

- * This feature is useful if you want to define your own process for deleting these (e.g. because - * you are running in a cluster) + * Search results are stored in the database so that they can be paged over multiple + * requests. After this + * number of milliseconds, they will be deleted from the database, and any paging links + * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. *

+ *

+ *

+ * To disable this feature entirely, see {@link #setExpireSearchResults(boolean)} + *

+ * + * @since 1.5 */ - public void setExpireSearchResults(boolean theDeleteStaleSearches) { - myDeleteStaleSearches = theDeleteStaleSearches; + public long getExpireSearchResultsAfterMillis() { + return myExpireSearchResultsAfterMillis; } /** @@ -613,17 +221,31 @@ public class DaoConfig { * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. *

*

- * + *

*

* To disable this feature entirely, see {@link #setExpireSearchResults(boolean)} *

- * + * * @since 1.5 */ public void setExpireSearchResultsAfterMillis(long theExpireSearchResultsAfterMillis) { myExpireSearchResultsAfterMillis = theExpireSearchResultsAfterMillis; } + /** + * Gets the default maximum number of results to load in a query. + *

+ * For example, if the database has a million Patient resources in it, and + * the client requests GET /Patient, if this value is set + * to a non-null value (default is null) only this number + * of results will be fetched. Setting this value appropriately + * can be useful to improve performance in some situations. + *

+ */ + public Integer getFetchSizeDefaultMaximum() { + return myFetchSizeDefaultMaximum; + } + /** * Gets the default maximum number of results to load in a query. *

@@ -639,16 +261,10 @@ public class DaoConfig { } /** - * Do not call this method, it exists only for legacy reasons. It - * will be removed in a future version. Configure the page size on your - * paging provider instead. - * - * @deprecated This method does not do anything. Configure the page size on your - * paging provider instead. Deprecated in HAPI FHIR 2.3 (Jan 2017) + * Gets the maximum number of results to return in a GetTags query (DSTU1 only) */ - @Deprecated - public void setHardSearchLimit(int theHardSearchLimit) { - // this method does nothing + public int getHardTagListLimit() { + return myHardTagListLimit; } /** @@ -658,6 +274,10 @@ public class DaoConfig { myHardTagListLimit = theHardTagListLimit; } + public int getIncludeLimit() { + return myIncludeLimit; + } + /** * This is the maximum number of resources that will be added to a single page of returned resources. Because of * includes with wildcards and other possibilities it is possible for a client to make requests that include very @@ -668,11 +288,15 @@ public class DaoConfig { } /** - * Should contained IDs be indexed the same way that non-contained IDs are (default is - * true) + * Returns the interceptors which will be notified of operations. + * + * @see #setInterceptors(List) */ - public void setIndexContainedResources(boolean theIndexContainedResources) { - myIndexContainedResources = theIndexContainedResources; + public List getInterceptors() { + if (myInterceptors == null) { + myInterceptors = new ArrayList<>(); + } + return myInterceptors; } /** @@ -686,10 +310,10 @@ public class DaoConfig { } /** - * This may be used to optionally register server interceptors directly against the DAOs. + * See {@link #setMaximumExpansionSize(int)} */ - public void setInterceptors(List theInterceptors) { - myInterceptors = theInterceptors; + public int getMaximumExpansionSize() { + return myMaximumExpansionSize; } /** @@ -704,22 +328,56 @@ public class DaoConfig { /** * Provides the maximum number of results which may be returned by a search (HTTP GET) which * is executed as a sub-operation within within a FHIR transaction or - * batch operation. For example, if this value is set to 100 and + * batch operation. For example, if this value is set to 100 and * a FHIR transaction is processed with a sub-request for Patient?gender=male, * the server will throw an error (and the transaction will fail) if there are more than * 100 resources on the server which match this query. *

* The default value is null, which means that there is no limit. - *

+ *

+ */ + public Integer getMaximumSearchResultCountInTransaction() { + return myMaximumSearchResultCountInTransaction; + } + + /** + * Provides the maximum number of results which may be returned by a search (HTTP GET) which + * is executed as a sub-operation within within a FHIR transaction or + * batch operation. For example, if this value is set to 100 and + * a FHIR transaction is processed with a sub-request for Patient?gender=male, + * the server will throw an error (and the transaction will fail) if there are more than + * 100 resources on the server which match this query. + *

+ * The default value is null, which means that there is no limit. + *

*/ public void setMaximumSearchResultCountInTransaction(Integer theMaximumSearchResultCountInTransaction) { myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction; } + public ResourceEncodingEnum getResourceEncoding() { + return myResourceEncoding; + } + public void setResourceEncoding(ResourceEncodingEnum theResourceEncoding) { myResourceEncoding = theResourceEncoding; } + /** + * If set, an individual resource will not be allowed to have more than the + * given number of tags, profiles, and security labels (the limit is for the combined + * total for all of these things on an individual resource). + *

+ * If set to null, no limit will be applied. + *

+ *

+ * The default value for this setting is 1000. + *

+ */ + public Integer getResourceMetaCountHardLimit() { + return myResourceMetaCountHardLimit; + } + /** * If set, an individual resource will not be allowed to have more than the * given number of tags, profiles, and security labels (the limit is for the combined @@ -735,6 +393,21 @@ public class DaoConfig { myResourceMetaCountHardLimit = theResourceMetaCountHardLimit; } + /** + * If set to a non {@literal null} value (default is {@link #DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS non null}) + * if an identical search is requested multiple times within this window, the same results will be returned + * to multiple queries. For example, if this value is set to 1 minute and a client searches for all + * patients named "smith", and then a second client also performs the same search within 1 minute, + * the same cached results will be returned. + *

+ * This approach can improve performance, especially under heavy load, but can also mean that + * searches may potentially return slightly out-of-date results. + *

+ */ + public Long getReuseCachedSearchResultsForMillis() { + return myReuseCachedSearchResultsForMillis; + } + /** * If set to a non {@literal null} value (default is {@link #DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS non null}) * if an identical search is requested multiple times within this window, the same results will be returned @@ -750,23 +423,18 @@ public class DaoConfig { myReuseCachedSearchResultsForMillis = theReuseCachedSearchResultsForMillis; } - public void setSchedulingDisabled(boolean theSchedulingDisabled) { - mySchedulingDisabled = theSchedulingDisabled; - } - - /** - * If set to true, the server will enable support for subscriptions. Subscriptions - * will by default be handled via a polling task. Note that if this is enabled, you must also include Spring task scanning to your XML - * config for the scheduled tasks used by the subscription module. - */ - public void setSubscriptionEnabled(boolean theSubscriptionEnabled) { - mySubscriptionEnabled = theSubscriptionEnabled; + public long getSubscriptionPollDelay() { + return mySubscriptionPollDelay; } public void setSubscriptionPollDelay(long theSubscriptionPollDelay) { mySubscriptionPollDelay = theSubscriptionPollDelay; } + public Long getSubscriptionPurgeInactiveAfterMillis() { + return mySubscriptionPurgeInactiveAfterMillis; + } + public void setSubscriptionPurgeInactiveAfterMillis(Long theMillis) { if (theMillis != null) { Validate.exclusiveBetween(0, Long.MAX_VALUE, theMillis); @@ -774,24 +442,6 @@ public class DaoConfig { mySubscriptionPurgeInactiveAfterMillis = theMillis; } - public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) { - setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND); - } - - /** - * If set to {@literal true} (default is true), if a client performs an update which does not actually - * result in any chance to a given resource (e.g. an update where the resource body matches the - * existing resource body in the database) the operation will succeed but a new version (and corresponding history - * entry) will not actually be created. The existing resource version will be returned to the client. - *

- * If set to {@literal false}, all updates will result in the creation of a new version - *

- */ - public void setSuppressUpdatesWithNoChange(boolean theSuppressUpdatesWithNoChange) { - mySuppressUpdatesWithNoChange = theSuppressUpdatesWithNoChange; - - } - /** * This setting may be used to advise the server that any references found in * resources that have any of the base URLs given here will be replaced with @@ -802,10 +452,29 @@ public class DaoConfig { * http://example.com/base/Patient/1, the server will automatically * convert this reference to Patient/1 *

- * - * @param theTreatBaseUrlsAsLocal - * The set of base URLs. May be null, which - * means no references will be treated as external + *

+ * Note that this property has different behaviour from {@link DaoConfig#getTreatReferencesAsLogical()} + *

+ * + * @see #getTreatReferencesAsLogical() + */ + public Set getTreatBaseUrlsAsLocal() { + return myTreatBaseUrlsAsLocal; + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

+ * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

+ * + * @param theTreatBaseUrlsAsLocal The set of base URLs. May be null, which + * means no references will be treated as external */ public void setTreatBaseUrlsAsLocal(Set theTreatBaseUrlsAsLocal) { if (theTreatBaseUrlsAsLocal != null) { @@ -842,7 +511,32 @@ public class DaoConfig { *
  • http://example.com/some-url (will be matched exactly)
  • *
  • http://example.com/some-base* (will match anything beginning with the part before the *)
  • * - * + * + * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property + */ + public Set getTreatReferencesAsLogical() { + return myTreatReferencesAsLogical; + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be treated as logical + * references instead of being treated as real references. + *

    + * A logical reference is a reference which is treated as an identifier, and + * does not neccesarily resolve. See {@link "http://hl7.org/fhir/references.html"} for + * a description of logical references. For example, the valueset + * {@link "http://hl7.org/fhir/valueset-quantity-comparator.html"} is a logical + * reference. + *

    + *

    + * Values for this field may take either of the following forms: + *

    + *
      + *
    • http://example.com/some-url (will be matched exactly)
    • + *
    • http://example.com/some-base* (will match anything beginning with the part before the *)
    • + *
    + * * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property */ public DaoConfig setTreatReferencesAsLogical(Set theTreatReferencesAsLogical) { @@ -850,6 +544,347 @@ public class DaoConfig { return this; } + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

    + * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

    + *

    + * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

    + *

    + * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

    + * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public boolean isAllowExternalReferences() { + return myAllowExternalReferences; + } + + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

    + * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

    + *

    + * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

    + *

    + * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

    + * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public void setAllowExternalReferences(boolean theAllowExternalReferences) { + myAllowExternalReferences = theAllowExternalReferences; + } + + /** + * @see #setAllowInlineMatchUrlReferences(boolean) + */ + public boolean isAllowInlineMatchUrlReferences() { + return myAllowInlineMatchUrlReferences; + } + + /** + * Should references containing match URLs be resolved and replaced in create and update operations. For + * example, if this property is set to true and a resource is created containing a reference + * to "Patient?identifier=12345", this is reference match URL will be resolved and replaced according + * to the usual match URL rules. + *

    + * Default is {@literal true} beginning in HAPI FHIR 2.4, since this + * feature is now specified in the FHIR specification. (Previously it + * was an experimental/rpposed feature) + *

    + * + * @since 1.5 + */ + public void setAllowInlineMatchUrlReferences(boolean theAllowInlineMatchUrlReferences) { + myAllowInlineMatchUrlReferences = theAllowInlineMatchUrlReferences; + } + + public boolean isAllowMultipleDelete() { + return myAllowMultipleDelete; + } + + public void setAllowMultipleDelete(boolean theAllowMultipleDelete) { + myAllowMultipleDelete = theAllowMultipleDelete; + } + + /** + * When creating or updating a resource: If this property is set to true + * (default is false), if the resource has a reference to another resource + * on the local server but that reference does not exist, a placeholder resource will be + * created. + *

    + * In other words, if an observation with subject Patient/FOO is created, but + * there is no resource called Patient/FOO on the server, this property causes + * an empty patient with ID "FOO" to be created in order to prevent this operation + * from failing. + *

    + *

    + * This property can be useful in cases where replication between two servers is wanted. + * Note however that references containing purely numeric IDs will not be auto-created + * as they are never allowed to be client supplied in HAPI FHIR JPA. + *

    + */ + public boolean isAutoCreatePlaceholderReferenceTargets() { + return myAutoCreatePlaceholderReferenceTargets; + } + + /** + * When creating or updating a resource: If this property is set to true + * (default is false), if the resource has a reference to another resource + * on the local server but that reference does not exist, a placeholder resource will be + * created. + *

    + * In other words, if an observation with subject Patient/FOO is created, but + * there is no resource called Patient/FOO on the server, this property causes + * an empty patient with ID "FOO" to be created in order to prevent this operation + * from failing. + *

    + *

    + * This property can be useful in cases where replication between two servers is wanted. + * Note however that references containing purely numeric IDs will not be auto-created + * as they are never allowed to be client supplied in HAPI FHIR JPA. + *

    + */ + public void setAutoCreatePlaceholderReferenceTargets(boolean theAutoCreatePlaceholderReferenceTargets) { + myAutoCreatePlaceholderReferenceTargets = theAutoCreatePlaceholderReferenceTargets; + } + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public boolean isDefaultSearchParamsCanBeOverridden() { + return myDefaultSearchParamsCanBeOverridden; + } + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public void setDefaultSearchParamsCanBeOverridden(boolean theDefaultSearchParamsCanBeOverridden) { + myDefaultSearchParamsCanBeOverridden = theDefaultSearchParamsCanBeOverridden; + } + + /** + * If set to false (default is true) resources will be permitted to be + * deleted even if other resources currently contain references to them. + *

    + * This property can cause confusing results for clients of the server since searches, includes, + * and other FHIR features may not behave as expected when referential integrity is not + * preserved. Use this feature with caution. + *

    + */ + public boolean isEnforceReferentialIntegrityOnDelete() { + return myEnforceReferentialIntegrityOnDelete; + } + + /** + * If set to false (default is true) resources will be permitted to be + * deleted even if other resources currently contain references to them. + *

    + * This property can cause confusing results for clients of the server since searches, includes, + * and other FHIR features may not behave as expected when referential integrity is not + * preserved. Use this feature with caution. + *

    + */ + public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) { + myEnforceReferentialIntegrityOnDelete = theEnforceReferentialIntegrityOnDelete; + } + + /** + * If set to false (default is true) resources will be permitted to be + * created or updated even if they contain references to local resources that do not exist. + *

    + * For example, if a patient contains a reference to managing organization Organization/FOO + * but FOO is not a valid ID for an organization on the server, the operation will be blocked unless + * this propery has been set to false + *

    + *

    + * This property can cause confusing results for clients of the server since searches, includes, + * and other FHIR features may not behave as expected when referential integrity is not + * preserved. Use this feature with caution. + *

    + */ + public boolean isEnforceReferentialIntegrityOnWrite() { + return myEnforceReferentialIntegrityOnWrite; + } + + /** + * If set to false (default is true) resources will be permitted to be + * created or updated even if they contain references to local resources that do not exist. + *

    + * For example, if a patient contains a reference to managing organization Organization/FOO + * but FOO is not a valid ID for an organization on the server, the operation will be blocked unless + * this propery has been set to false + *

    + *

    + * This property can cause confusing results for clients of the server since searches, includes, + * and other FHIR features may not behave as expected when referential integrity is not + * preserved. Use this feature with caution. + *

    + */ + public void setEnforceReferentialIntegrityOnWrite(boolean theEnforceReferentialIntegrityOnWrite) { + myEnforceReferentialIntegrityOnWrite = theEnforceReferentialIntegrityOnWrite; + } + + /** + * If this is set to false (default is true) the stale search deletion + * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. + *

    + * This feature is useful if you want to define your own process for deleting these (e.g. because + * you are running in a cluster) + *

    + */ + public boolean isExpireSearchResults() { + return myDeleteStaleSearches; + } + + /** + * If this is set to false (default is true) the stale search deletion + * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. + *

    + * This feature is useful if you want to define your own process for deleting these (e.g. because + * you are running in a cluster) + *

    + */ + public void setExpireSearchResults(boolean theDeleteStaleSearches) { + myDeleteStaleSearches = theDeleteStaleSearches; + } + + /** + * Should contained IDs be indexed the same way that non-contained IDs are (default is + * true) + */ + public boolean isIndexContainedResources() { + return myIndexContainedResources; + } + + /** + * Should contained IDs be indexed the same way that non-contained IDs are (default is + * true) + */ + public void setIndexContainedResources(boolean theIndexContainedResources) { + myIndexContainedResources = theIndexContainedResources; + } + + public boolean isSchedulingDisabled() { + return mySchedulingDisabled; + } + + public void setSchedulingDisabled(boolean theSchedulingDisabled) { + mySchedulingDisabled = theSchedulingDisabled; + } + + /** + * See {@link #setSubscriptionEnabled(boolean)} + */ + public boolean isSubscriptionEnabled() { + return mySubscriptionEnabled; + } + + /** + * If set to true, the server will enable support for subscriptions. Subscriptions + * will by default be handled via a polling task. Note that if this is enabled, you must also include Spring task scanning to your XML + * config for the scheduled tasks used by the subscription module. + */ + public void setSubscriptionEnabled(boolean theSubscriptionEnabled) { + mySubscriptionEnabled = theSubscriptionEnabled; + } + + /** + * If set to {@literal true} (default is true), if a client performs an update which does not actually + * result in any chance to a given resource (e.g. an update where the resource body matches the + * existing resource body in the database) the operation will succeed but a new version (and corresponding history + * entry) will not actually be created. The existing resource version will be returned to the client. + *

    + * If set to {@literal false}, all updates will result in the creation of a new version + *

    + */ + public boolean isSuppressUpdatesWithNoChange() { + return mySuppressUpdatesWithNoChange; + } + + /** + * If set to {@literal true} (default is true), if a client performs an update which does not actually + * result in any chance to a given resource (e.g. an update where the resource body matches the + * existing resource body in the database) the operation will succeed but a new version (and corresponding history + * entry) will not actually be created. The existing resource version will be returned to the client. + *

    + * If set to {@literal false}, all updates will result in the creation of a new version + *

    + */ + public void setSuppressUpdatesWithNoChange(boolean theSuppressUpdatesWithNoChange) { + mySuppressUpdatesWithNoChange = theSuppressUpdatesWithNoChange; + + } + + /** + * Do not call this method, it exists only for legacy reasons. It + * will be removed in a future version. Configure the page size on your + * paging provider instead. + * + * @deprecated This method does not do anything. Configure the page size on your + * paging provider instead. Deprecated in HAPI FHIR 2.3 (Jan 2017) + */ + @Deprecated + public void setHardSearchLimit(int theHardSearchLimit) { + // this method does nothing + } + + /** + * This may be used to optionally register server interceptors directly against the DAOs. + */ + public void setInterceptors(List theInterceptors) { + myInterceptors = theInterceptors; + } + + public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) { + setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND); + } + private static void validateTreatBaseUrlsAsLocal(String theUrl) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java new file mode 100644 index 00000000000..71aa00512fe --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java @@ -0,0 +1,140 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.hamcrest.Matchers; +import org.hamcrest.core.StringContains; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.hl7.fhir.r4.model.OperationOutcome.IssueType; +import org.hl7.fhir.r4.model.Quantity.QuantityComparator; +import org.junit.*; +import org.mockito.ArgumentCaptor; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +@SuppressWarnings({ "unchecked", "deprecation" }) +public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class); + + @After + public final void afterResetDao() { + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); + } + + @Test + public void testCreateWithBadReferenceFails() { + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/FOO"); + try { + myObservationDao.create(o, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource Patient/FOO not found, specified in path: Observation.subject", e.getMessage()); + } + } + + @Test + public void testCreateWithBadReferenceIsPermitted() { + assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/FOO"); + try { + myObservationDao.create(o, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource Patient/FOO not found, specified in path: Observation.subject", e.getMessage()); + } + } + + @Test + public void testUpdateWithBadReferenceFails() { + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + IIdType id = myObservationDao.create(o, mySrd).getId(); + + o = new Observation(); + o.setId(id); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/FOO"); + try { + myObservationDao.update(o, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource Patient/FOO not found, specified in path: Observation.subject", e.getMessage()); + } + } + + @Test + public void testUpdateWithBadReferenceIsPermitted() { + assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + IIdType id = myObservationDao.create(o, mySrd).getId(); + + o = new Observation(); + o.setId(id); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/FOO"); + try { + myObservationDao.update(o, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource Patient/FOO not found, specified in path: Observation.subject", e.getMessage()); + } + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index e3b5d630876..b17cdd4d815 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -337,6 +337,11 @@ to Eugene Lubarsky for the pull request, and for convincing James not to optimize something that did not need optimizing! + + A new config property has been added to the JPA seerver DaoConfig called "setAutoCreatePlaceholderReferenceTargets". + This property causes references to unknown resources in created/updated resources to have a placeholder + target resource automatically created. +