Merge branch 'master' into 2958-consistent-error-handling
This commit is contained in:
commit
a02a9e0254
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -961,6 +961,7 @@ public class FhirTerser {
|
|||
for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) {
|
||||
|
||||
List<?> values = nextChild.getAccessor().getValues(theElement);
|
||||
|
||||
if (values != null) {
|
||||
for (Object nextValueObject : values) {
|
||||
IBase nextValue;
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-bom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>HAPI FHIR BOM</name>
|
||||
|
||||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -282,7 +282,7 @@ public abstract class BaseApp {
|
|||
}
|
||||
|
||||
private Optional<BaseCommand> parseCommand(String[] theArgs) {
|
||||
Optional<BaseCommand> commandOpt = getNextCommand(theArgs);
|
||||
Optional<BaseCommand> commandOpt = getNextCommand(theArgs, 0);
|
||||
|
||||
if (! commandOpt.isPresent()) {
|
||||
String message = "Unrecognized command: " + ansi().bold().fg(Ansi.Color.RED) + theArgs[0] + ansi().boldOff().fg(Ansi.Color.WHITE);
|
||||
|
@ -294,8 +294,8 @@ public abstract class BaseApp {
|
|||
return commandOpt;
|
||||
}
|
||||
|
||||
private Optional<BaseCommand> getNextCommand(String[] theArgs) {
|
||||
return ourCommands.stream().filter(cmd -> cmd.getCommandName().equals(theArgs[0])).findFirst();
|
||||
private Optional<BaseCommand> getNextCommand(String[] theArgs, int thePosition) {
|
||||
return ourCommands.stream().filter(cmd -> cmd.getCommandName().equals(theArgs[thePosition])).findFirst();
|
||||
}
|
||||
|
||||
private void processHelp(String[] theArgs) {
|
||||
|
@ -303,7 +303,7 @@ public abstract class BaseApp {
|
|||
logUsage();
|
||||
return;
|
||||
}
|
||||
Optional<BaseCommand> commandOpt = getNextCommand(theArgs);
|
||||
Optional<BaseCommand> commandOpt = getNextCommand(theArgs, 1);
|
||||
if (! commandOpt.isPresent()) {
|
||||
String message = "Unknown command: " + theArgs[1];
|
||||
System.err.println(message);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package ca.uhn.fhir.cli;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
public class BaseAppTest {
|
||||
|
||||
private final PrintStream standardOut = System.out;
|
||||
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
System.setOut(new PrintStream(outputStreamCaptor));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
System.setOut(standardOut);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHelpOption() {
|
||||
App.main(new String[]{"help", "create-package"});
|
||||
assertThat(outputStreamCaptor.toString().trim(), outputStreamCaptor.toString().trim(), containsString("Usage"));
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-cli</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.apache.commons.lang3.time.DateUtils;
|
|||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -65,8 +66,8 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("HAPI_PU");
|
||||
retVal.setDataSource(myDataSource);
|
||||
retVal.setJpaProperties(myJpaProperties);
|
||||
|
|
|
@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
|||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -62,8 +63,8 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("HAPI_PU");
|
||||
retVal.setDataSource(myDataSource);
|
||||
retVal.setJpaProperties(myJpaProperties);
|
||||
|
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
|||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -60,8 +61,8 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("HAPI_PU");
|
||||
retVal.setDataSource(myDataSource);
|
||||
retVal.setJpaProperties(myJpaProperties);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 2901
|
||||
jira: SMILE-3004
|
||||
title: "Processing transactions with AutoversionAtPaths set should create those resources (if AutoCreatePlaceholders is set) and use latest version as expected"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 2962
|
||||
jira: SMILE-720
|
||||
title: "Added a new DaoConfig setting called `setElasticSearchIndexPrefix(String prefix)` which will cause Hibernate search to prefix all of its tables with the provided value."
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 2967
|
||||
jira: SMILE-2899
|
||||
title: "Previously, the system would only traverse references to discrete resources while performing a chained search.
|
||||
This fix adds support for traversing references to contained resources as well, with the limitation that the reference
|
||||
to the contained resource must be the last reference in the chain."
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 2973
|
||||
title: "CLI `smileutil help {command}` returns `Unknown command` which should return the usage of `command`. This has been corrected."
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2975
|
||||
title: "Two improvements have been made to the connection to Elasticsearch. First, null username and password values are now permitted. Second, multiple hosts are now permitted via the `setHosts()` method on the ElasticHibernatePropertiesBuilder, allowing you to
|
||||
connect to multiple elasticsearch clusters at once. Thanks to Dušan Marković for the contribution!"
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
type: fix
|
||||
title: "Fixed a bug where two identical tags in parallel entries being created in a batch would fail."
|
|
@ -31,12 +31,11 @@ In addition, the Elasticsearch client service, `ElasticsearchSvcImpl` will need
|
|||
```java
|
||||
@Bean()
|
||||
public ElasticsearchSvcImpl elasticsearchSvc() {
|
||||
String elasticsearchHost = "localhost";
|
||||
String elasticsearchUserId = "elastic";
|
||||
String elasticsearchHost = "localhost:9200";
|
||||
String elasticsearchUsername = "elastic";
|
||||
String elasticsearchPassword = "changeme";
|
||||
int elasticsearchPort = 9301;
|
||||
|
||||
return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUserId, elasticsearchPassword);
|
||||
return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchUsername, elasticsearchPassword);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -302,6 +302,14 @@ If the server has been configured with a [Resource Server ID Strategy](/apidocs/
|
|||
Contains the specific version (starting with 1) of the resource that this row corresponds to.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RESOURCE_TYPE</td>
|
||||
<td></td>
|
||||
<td>String</td>
|
||||
<td>
|
||||
Contains the string specifying the type of the resource (Patient, Observation, etc).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -476,7 +484,7 @@ The following columns are common to **all HFJ_SPIDX_xxx tables**.
|
|||
<tr>
|
||||
<td>RES_ID</td>
|
||||
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
|
||||
<td>String</td>
|
||||
<td>Long</td>
|
||||
<td></td>
|
||||
<td>
|
||||
Contains the PID of the resource being indexed.
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -258,6 +258,11 @@ public class DaoConfig {
|
|||
private boolean myAccountForDateIndexNulls;
|
||||
private boolean myTriggerSubscriptionsForNonVersioningChanges;
|
||||
|
||||
/**
|
||||
* @since 5.6.0
|
||||
*/
|
||||
private String myElasicSearchIndexPrefix;
|
||||
|
||||
/**
|
||||
* @since 5.6.0
|
||||
*/
|
||||
|
@ -269,6 +274,7 @@ public class DaoConfig {
|
|||
private Integer myBundleBatchPoolSize = DEFAULT_BUNDLE_BATCH_POOL_SIZE;
|
||||
private Integer myBundleBatchMaxPoolSize = DEFAULT_BUNDLE_BATCH_MAX_POOL_SIZE;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
|
@ -2643,7 +2649,29 @@ public class DaoConfig {
|
|||
return retval;
|
||||
}
|
||||
|
||||
public enum StoreMetaSourceInformationEnum {
|
||||
/**
|
||||
*
|
||||
* Sets a prefix for any indexes created when interacting with elasticsearch. This will apply to fulltext search indexes
|
||||
* and terminology expansion indexes.
|
||||
*
|
||||
* @since 5.6.0
|
||||
*/
|
||||
public String getElasticSearchIndexPrefix() {
|
||||
return myElasicSearchIndexPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets a prefix for any indexes created when interacting with elasticsearch. This will apply to fulltext search indexes
|
||||
* and terminology expansion indexes.
|
||||
*
|
||||
* @since 5.6.0
|
||||
*/
|
||||
public void setElasticSearchIndexPrefix(String thePrefix) {
|
||||
myElasicSearchIndexPrefix = thePrefix;
|
||||
}
|
||||
|
||||
public enum StoreMetaSourceInformationEnum {
|
||||
NONE(false, false),
|
||||
SOURCE_URI(true, false),
|
||||
REQUEST_ID(false, true),
|
||||
|
|
|
@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.api.dao;
|
|||
|
||||
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
|
||||
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
|
||||
import ca.uhn.fhir.rest.annotation.Offset;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -24,18 +24,23 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
|||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.util.QueryChunker;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
@ -52,17 +57,19 @@ public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
|
|||
DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
IResourceTableDao myResourceTableDao;
|
||||
@Autowired
|
||||
IdHelperService myIdHelperService;
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
public ResourceVersionMap getVersionMap(RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
|
||||
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("About to retrieve version map for resource type: {}", theResourceName);
|
||||
}
|
||||
|
||||
List<Long> matchingIds = dao.searchForIds(theSearchParamMap, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.allPartitions())).stream()
|
||||
List<Long> matchingIds = dao.searchForIds(theSearchParamMap, new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId)).stream()
|
||||
.map(ResourcePersistentId::getIdAsLong)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
@ -74,4 +81,95 @@ public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
|
|||
|
||||
return ResourceVersionMap.fromResourceTableEntities(allById);
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Retrieves the latest versions for any resourceid that are found.
|
||||
* If they are not found, they will not be contained in the returned map.
|
||||
* The key should be the same value that was passed in to allow
|
||||
* consumer to look up the value using the id they already have.
|
||||
*
|
||||
* This method should not throw, so it can safely be consumed in
|
||||
* transactions.
|
||||
*
|
||||
* @param theRequestPartitionId - request partition id
|
||||
* @param theIds - list of IIdTypes for resources of interest.
|
||||
* @return
|
||||
*/
|
||||
public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
|
||||
ResourcePersistentIdMap idToPID = new ResourcePersistentIdMap();
|
||||
HashMap<String, List<IIdType>> resourceTypeToIds = new HashMap<>();
|
||||
|
||||
for (IIdType id : theIds) {
|
||||
String resourceType = id.getResourceType();
|
||||
if (!resourceTypeToIds.containsKey(resourceType)) {
|
||||
resourceTypeToIds.put(resourceType, new ArrayList<>());
|
||||
}
|
||||
resourceTypeToIds.get(resourceType).add(id);
|
||||
}
|
||||
|
||||
for (String resourceType : resourceTypeToIds.keySet()) {
|
||||
ResourcePersistentIdMap idAndPID = getIdsOfExistingResources(theRequestPartitionId,
|
||||
resourceTypeToIds.get(resourceType));
|
||||
idToPID.putAll(idAndPID);
|
||||
}
|
||||
|
||||
return idToPID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to determine if some resources exist in the DB (without throwing).
|
||||
* Returns a set that contains the IIdType for every resource found.
|
||||
* If it's not found, it won't be included in the set.
|
||||
*
|
||||
* @param theIds - list of IIdType ids (for the same resource)
|
||||
* @return
|
||||
*/
|
||||
private ResourcePersistentIdMap getIdsOfExistingResources(RequestPartitionId thePartitionId,
|
||||
Collection<IIdType> theIds) {
|
||||
// these are the found Ids that were in the db
|
||||
ResourcePersistentIdMap retval = new ResourcePersistentIdMap();
|
||||
|
||||
if (theIds == null || theIds.isEmpty()) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
List<ResourcePersistentId> resourcePersistentIds = myIdHelperService.resolveResourcePersistentIdsWithCache(thePartitionId,
|
||||
theIds.stream().collect(Collectors.toList()));
|
||||
|
||||
// we'll use this map to fetch pids that require versions
|
||||
HashMap<Long, ResourcePersistentId> pidsToVersionToResourcePid = new HashMap<>();
|
||||
|
||||
// fill in our map
|
||||
for (ResourcePersistentId pid : resourcePersistentIds) {
|
||||
if (pid.getVersion() == null) {
|
||||
pidsToVersionToResourcePid.put(pid.getIdAsLong(), pid);
|
||||
}
|
||||
Optional<IIdType> idOp = theIds.stream()
|
||||
.filter(i -> i.getIdPart().equals(pid.getAssociatedResourceId().getIdPart()))
|
||||
.findFirst();
|
||||
// this should always be present
|
||||
// since it was passed in.
|
||||
// but land of optionals...
|
||||
idOp.ifPresent(id -> {
|
||||
retval.put(id, pid);
|
||||
});
|
||||
}
|
||||
|
||||
// set any versions we don't already have
|
||||
if (!pidsToVersionToResourcePid.isEmpty()) {
|
||||
Collection<Object[]> resourceEntries = myResourceTableDao
|
||||
.getResourceVersionsForPid(new ArrayList<>(pidsToVersionToResourcePid.keySet()));
|
||||
|
||||
for (Object[] record : resourceEntries) {
|
||||
// order matters!
|
||||
Long retPid = (Long) record[0];
|
||||
String resType = (String) record[1];
|
||||
Long version = (Long) record[2];
|
||||
pidsToVersionToResourcePid.get(retPid).setVersion(version);
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,7 @@ import ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl;
|
|||
import ca.uhn.fhir.jpa.search.cache.DatabaseSearchResultCacheSvcImpl;
|
||||
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
|
||||
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
|
||||
import ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy;
|
||||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexer;
|
||||
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
|
||||
|
@ -155,6 +156,7 @@ import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
|
|||
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
|
||||
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -288,8 +290,8 @@ public abstract class BaseConfig {
|
|||
* bean, but it provides a partially completed entity manager
|
||||
* factory with HAPI FHIR customizations
|
||||
*/
|
||||
protected LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = new HapiFhirLocalContainerEntityManagerFactoryBean();
|
||||
protected LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory myConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = new HapiFhirLocalContainerEntityManagerFactoryBean(myConfigurableListableBeanFactory);
|
||||
configureEntityManagerFactory(retVal, fhirContext());
|
||||
return retVal;
|
||||
}
|
||||
|
@ -919,6 +921,11 @@ public abstract class BaseConfig {
|
|||
return new PredicateBuilderFactory(theApplicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IndexNamePrefixLayoutStrategy indexLayoutStrategy() {
|
||||
return new IndexNamePrefixLayoutStrategy();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JpaResourceLoader jpaResourceLoader() {
|
||||
return new JpaResourceLoader();
|
||||
|
|
|
@ -23,6 +23,9 @@ package ca.uhn.fhir.jpa.config;
|
|||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.orm.hibernate5.SpringBeanContainer;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
|
||||
import java.util.Map;
|
||||
|
@ -32,6 +35,14 @@ import java.util.Map;
|
|||
* that sets some sensible default property values
|
||||
*/
|
||||
public class HapiFhirLocalContainerEntityManagerFactoryBean extends LocalContainerEntityManagerFactoryBean {
|
||||
|
||||
//https://stackoverflow.com/questions/57902388/how-to-inject-spring-beans-into-the-hibernate-envers-revisionlistener
|
||||
ConfigurableListableBeanFactory myConfigurableListableBeanFactory;
|
||||
|
||||
public HapiFhirLocalContainerEntityManagerFactoryBean(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
myConfigurableListableBeanFactory = theConfigurableListableBeanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getJpaPropertyMap() {
|
||||
Map<String, Object> retVal = super.getJpaPropertyMap();
|
||||
|
@ -63,6 +74,11 @@ public class HapiFhirLocalContainerEntityManagerFactoryBean extends LocalContain
|
|||
if (!retVal.containsKey(AvailableSettings.BATCH_VERSIONED_DATA)) {
|
||||
retVal.put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
|
||||
}
|
||||
// Why is this here, you ask? LocalContainerEntityManagerFactoryBean actually clobbers the setting hibernate needs
|
||||
// in order to be able to resolve beans, so we add it back in manually here
|
||||
if (!retVal.containsKey(AvailableSettings.BEAN_CONTAINER)) {
|
||||
retVal.put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(myConfigurableListableBeanFactory));
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
|
|
@ -1207,7 +1207,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
|||
if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) {
|
||||
|
||||
newParams = new ResourceIndexedSearchParams();
|
||||
|
||||
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing);
|
||||
|
||||
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
|
||||
|
|
|
@ -136,9 +136,12 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
|
|
@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache;
|
|||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
|
|
@ -25,19 +25,18 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
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.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
||||
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterAnd;
|
||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
||||
|
@ -45,12 +44,16 @@ import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||
import ca.uhn.fhir.rest.param.QualifierDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.OperationOutcomeUtil;
|
||||
|
@ -91,6 +94,10 @@ public abstract class BaseStorageDao {
|
|||
protected DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
protected ModelConfig myModelConfig;
|
||||
@Autowired
|
||||
protected IResourceVersionSvc myResourceVersionSvc;
|
||||
@Autowired
|
||||
protected DaoConfig myDaoConfig;
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
|
||||
|
@ -204,10 +211,33 @@ public abstract class BaseStorageDao {
|
|||
for (IBaseReference nextReference : referencesToVersion) {
|
||||
IIdType referenceElement = nextReference.getReferenceElement();
|
||||
if (!referenceElement.hasBaseUrl()) {
|
||||
String resourceType = referenceElement.getResourceType();
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
String targetVersionId = dao.getCurrentVersionId(referenceElement);
|
||||
String newTargetReference = referenceElement.withVersion(targetVersionId).getValue();
|
||||
|
||||
ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
|
||||
Collections.singletonList(referenceElement)
|
||||
);
|
||||
|
||||
// 3 cases:
|
||||
// 1) there exists a resource in the db with some version (use this version)
|
||||
// 2) no resource exists, but we will create one (eventually). The version is 1
|
||||
// 3) no resource exists, and none will be made -> throw
|
||||
Long version;
|
||||
if (resourceVersionMap.containsKey(referenceElement)) {
|
||||
// the resource exists... latest id
|
||||
// will be the value in the ResourcePersistentId
|
||||
version = resourceVersionMap.getResourcePersistentId(referenceElement).getVersion();
|
||||
} else if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
|
||||
// if idToPID doesn't contain object
|
||||
// but autcreateplaceholders is on
|
||||
// then the version will be 1 (the first version)
|
||||
version = 1L;
|
||||
}
|
||||
else {
|
||||
// resource not found
|
||||
// and no autocreateplaceholders set...
|
||||
// we throw
|
||||
throw new ResourceNotFoundException(referenceElement);
|
||||
}
|
||||
String newTargetReference = referenceElement.withVersion(version.toString()).getValue();
|
||||
nextReference.setReference(newTargetReference);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ 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.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
|
@ -34,6 +35,8 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
|||
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
|
||||
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
|
||||
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
||||
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
|
||||
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
|
||||
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
|
||||
|
@ -68,11 +71,11 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
|||
import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.rest.server.util.ServletRequestUtil;
|
||||
import ca.uhn.fhir.util.AsyncUtil;
|
||||
import ca.uhn.fhir.util.ElementUtil;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.ResourceReferenceInfo;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import ca.uhn.fhir.util.AsyncUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
|
@ -90,7 +93,6 @@ import org.hl7.fhir.instance.model.api.IBaseReference;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -117,11 +119,11 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.util.StringUtil.toUtf8String;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
|
@ -157,6 +159,9 @@ public abstract class BaseTransactionProcessor {
|
|||
|
||||
private TaskExecutor myExecutor ;
|
||||
|
||||
@Autowired
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
|
||||
@VisibleForTesting
|
||||
public void setDaoConfig(DaoConfig theDaoConfig) {
|
||||
myDaoConfig = theDaoConfig;
|
||||
|
@ -252,8 +257,10 @@ public abstract class BaseTransactionProcessor {
|
|||
myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
|
||||
}
|
||||
|
||||
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
|
||||
IBase newEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) {
|
||||
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome,
|
||||
IIdType nextResourceId, DaoMethodOutcome outcome,
|
||||
IBase newEntry, String theResourceType,
|
||||
IBaseResource theRes, RequestDetails theRequestDetails) {
|
||||
IIdType newId = outcome.getId().toUnqualified();
|
||||
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
|
||||
if (newId.equals(resourceId) == false) {
|
||||
|
@ -359,8 +366,8 @@ public abstract class BaseTransactionProcessor {
|
|||
IBase nextRequestEntry = null;
|
||||
for (int i=0; i<requestEntriesSize; i++ ) {
|
||||
nextRequestEntry = requestEntries.get(i);
|
||||
BundleTask bundleTask = new BundleTask(completionLatch, theRequestDetails, responseMap, i, nextRequestEntry, theNestedMode);
|
||||
getTaskExecutor().execute(bundleTask);
|
||||
RetriableBundleTask retriableBundleTask = new RetriableBundleTask(completionLatch, theRequestDetails, responseMap, i, nextRequestEntry, theNestedMode);
|
||||
getTaskExecutor().execute(retriableBundleTask);
|
||||
}
|
||||
|
||||
// waiting for all tasks to be completed
|
||||
|
@ -394,7 +401,8 @@ public abstract class BaseTransactionProcessor {
|
|||
myHapiTransactionService = theHapiTransactionService;
|
||||
}
|
||||
|
||||
private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest, final String theActionName, boolean theNestedMode) {
|
||||
private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest,
|
||||
final String theActionName, boolean theNestedMode) {
|
||||
validateDependencies();
|
||||
|
||||
String transactionType = myVersionAdapter.getBundleType(theRequest);
|
||||
|
@ -412,7 +420,8 @@ public abstract class BaseTransactionProcessor {
|
|||
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
|
||||
}
|
||||
|
||||
int numberOfEntries = myVersionAdapter.getEntries(theRequest).size();
|
||||
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
|
||||
int numberOfEntries = requestEntries.size();
|
||||
|
||||
if (myDaoConfig.getMaximumTransactionBundleSize() != null && numberOfEntries > myDaoConfig.getMaximumTransactionBundleSize()) {
|
||||
throw new PayloadTooLargeException("Transaction Bundle Too large. Transaction bundle contains " +
|
||||
|
@ -425,8 +434,6 @@ public abstract class BaseTransactionProcessor {
|
|||
final TransactionDetails transactionDetails = new TransactionDetails();
|
||||
final StopWatch transactionStopWatch = new StopWatch();
|
||||
|
||||
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
|
||||
|
||||
// Do all entries have a verb?
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
IBase nextReqEntry = requestEntries.get(i);
|
||||
|
@ -450,10 +457,11 @@ public abstract class BaseTransactionProcessor {
|
|||
List<IBase> getEntries = new ArrayList<>();
|
||||
final IdentityHashMap<IBase, Integer> originalRequestOrder = new IdentityHashMap<>();
|
||||
for (int i = 0; i < requestEntries.size(); i++) {
|
||||
originalRequestOrder.put(requestEntries.get(i), i);
|
||||
IBase requestEntry = requestEntries.get(i);
|
||||
originalRequestOrder.put(requestEntry, i);
|
||||
myVersionAdapter.addEntry(response);
|
||||
if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntries.get(i)).equals("GET")) {
|
||||
getEntries.add(requestEntries.get(i));
|
||||
if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntry).equals("GET")) {
|
||||
getEntries.add(requestEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -472,73 +480,17 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
entries.sort(new TransactionSorter(placeholderIds));
|
||||
|
||||
doTransactionWriteOperations(theRequestDetails, theActionName, transactionDetails, transactionStopWatch, response, originalRequestOrder, entries);
|
||||
// perform all writes
|
||||
doTransactionWriteOperations(theRequestDetails, theActionName,
|
||||
transactionDetails, transactionStopWatch,
|
||||
response, originalRequestOrder, entries);
|
||||
|
||||
/*
|
||||
* Loop through the request and process any entries of type GET
|
||||
*/
|
||||
if (getEntries.size() > 0) {
|
||||
transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries");
|
||||
}
|
||||
for (IBase nextReqEntry : getEntries) {
|
||||
|
||||
if (theNestedMode) {
|
||||
throw new InvalidRequestException("Can not invoke read operation on nested transaction");
|
||||
}
|
||||
|
||||
if (!(theRequestDetails instanceof ServletRequestDetails)) {
|
||||
throw new MethodNotAllowedException("Can not call transaction GET methods from this context");
|
||||
}
|
||||
|
||||
ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails;
|
||||
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
|
||||
IBase nextRespEntry = (IBase) myVersionAdapter.getEntries(response).get(originalOrder);
|
||||
|
||||
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
|
||||
|
||||
String transactionUrl = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
|
||||
|
||||
ServletSubRequestDetails requestDetails = ServletRequestUtil.getServletSubRequestDetails(srd, transactionUrl, paramValues);
|
||||
|
||||
String url = requestDetails.getRequestPath();
|
||||
|
||||
BaseMethodBinding<?> method = srd.getServer().determineResourceMethod(requestDetails, url);
|
||||
if (method == null) {
|
||||
throw new IllegalArgumentException("Unable to handle GET " + url);
|
||||
}
|
||||
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
|
||||
}
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
|
||||
}
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
|
||||
}
|
||||
|
||||
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
|
||||
try {
|
||||
|
||||
BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
|
||||
requestDetails.setRestOperationType(methodBinding.getRestOperationType());
|
||||
|
||||
IBaseResource resource = methodBinding.doInvokeServer(srd.getServer(), requestDetails);
|
||||
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
|
||||
resource = filterNestedBundle(requestDetails, resource);
|
||||
}
|
||||
myVersionAdapter.setResource(nextRespEntry, resource);
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
|
||||
} catch (NotModifiedException e) {
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
|
||||
} catch (BaseServerResponseException e) {
|
||||
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
|
||||
populateEntryWithOperationOutcome(e, nextRespEntry);
|
||||
}
|
||||
|
||||
}
|
||||
transactionStopWatch.endCurrentTask();
|
||||
// perform all gets
|
||||
// (we do these last so that the gets happen on the final state of the DB;
|
||||
// see above note)
|
||||
doTransactionReadOperations(theRequestDetails, response,
|
||||
getEntries, originalRequestOrder,
|
||||
transactionStopWatch, theNestedMode);
|
||||
|
||||
// Interceptor broadcast: JPA_PERFTRACE_INFO
|
||||
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) {
|
||||
|
@ -555,6 +507,74 @@ public abstract class BaseTransactionProcessor {
|
|||
return response;
|
||||
}
|
||||
|
||||
private void doTransactionReadOperations(final RequestDetails theRequestDetails, IBaseBundle theResponse,
|
||||
List<IBase> theGetEntries, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||
StopWatch theTransactionStopWatch, boolean theNestedMode) {
|
||||
if (theGetEntries.size() > 0) {
|
||||
theTransactionStopWatch.startTask("Process " + theGetEntries.size() + " GET entries");
|
||||
|
||||
/*
|
||||
* Loop through the request and process any entries of type GET
|
||||
*/
|
||||
for (IBase nextReqEntry : theGetEntries) {
|
||||
if (theNestedMode) {
|
||||
throw new InvalidRequestException("Can not invoke read operation on nested transaction");
|
||||
}
|
||||
|
||||
if (!(theRequestDetails instanceof ServletRequestDetails)) {
|
||||
throw new MethodNotAllowedException("Can not call transaction GET methods from this context");
|
||||
}
|
||||
|
||||
ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails;
|
||||
Integer originalOrder = theOriginalRequestOrder.get(nextReqEntry);
|
||||
IBase nextRespEntry = (IBase) myVersionAdapter.getEntries(theResponse).get(originalOrder);
|
||||
|
||||
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
|
||||
|
||||
String transactionUrl = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
|
||||
|
||||
ServletSubRequestDetails requestDetails = ServletRequestUtil.getServletSubRequestDetails(srd, transactionUrl, paramValues);
|
||||
|
||||
String url = requestDetails.getRequestPath();
|
||||
|
||||
BaseMethodBinding<?> method = srd.getServer().determineResourceMethod(requestDetails, url);
|
||||
if (method == null) {
|
||||
throw new IllegalArgumentException("Unable to handle GET " + url);
|
||||
}
|
||||
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
|
||||
}
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
|
||||
}
|
||||
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
|
||||
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
|
||||
}
|
||||
|
||||
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
|
||||
try {
|
||||
BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
|
||||
requestDetails.setRestOperationType(methodBinding.getRestOperationType());
|
||||
|
||||
IBaseResource resource = methodBinding.doInvokeServer(srd.getServer(), requestDetails);
|
||||
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
|
||||
resource = filterNestedBundle(requestDetails, resource);
|
||||
}
|
||||
myVersionAdapter.setResource(nextRespEntry, resource);
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
|
||||
} catch (NotModifiedException e) {
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
|
||||
} catch (BaseServerResponseException e) {
|
||||
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
|
||||
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
|
||||
populateEntryWithOperationOutcome(e, nextRespEntry);
|
||||
}
|
||||
}
|
||||
theTransactionStopWatch.endCurrentTask();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All of the write operations in the transaction (PUT, POST, etc.. basically anything
|
||||
* except GET) are performed in their own database transaction before we do the reads.
|
||||
|
@ -564,7 +584,10 @@ public abstract class BaseTransactionProcessor {
|
|||
* heavy load with lots of concurrent transactions using all available
|
||||
* database connections.
|
||||
*/
|
||||
private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName, TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries) {
|
||||
private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName,
|
||||
TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch,
|
||||
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||
List<IBase> theEntries) {
|
||||
TransactionWriteOperationsDetails writeOperationsDetails = null;
|
||||
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) ||
|
||||
CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, myInterceptorBroadcaster, theRequestDetails)) {
|
||||
|
@ -593,14 +616,18 @@ public abstract class BaseTransactionProcessor {
|
|||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(TransactionWriteOperationsDetails.class, writeOperationsDetails);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, params);
|
||||
|
||||
}
|
||||
|
||||
TransactionCallback<Map<IBase, IIdType>> txCallback = status -> {
|
||||
final Set<IIdType> allIds = new LinkedHashSet<>();
|
||||
final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
|
||||
final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
|
||||
Map<IBase, IIdType> retVal = doTransactionWriteOperations(theRequestDetails, theActionName, theTransactionDetails, allIds, idSubstitutions, idToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch);
|
||||
|
||||
Map<IBase, IIdType> retVal = doTransactionWriteOperations(theRequestDetails, theActionName,
|
||||
theTransactionDetails, allIds,
|
||||
idSubstitutions, idToPersistedOutcome,
|
||||
theResponse, theOriginalRequestOrder,
|
||||
theEntries, theTransactionStopWatch);
|
||||
|
||||
theTransactionStopWatch.startTask("Commit writes to database");
|
||||
return retVal;
|
||||
|
@ -609,7 +636,8 @@ public abstract class BaseTransactionProcessor {
|
|||
|
||||
try {
|
||||
entriesToProcess = myHapiTransactionService.execute(theRequestDetails, theTransactionDetails, txCallback);
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
if (writeOperationsDetails != null) {
|
||||
HookParams params = new HookParams()
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
|
@ -664,8 +692,129 @@ public abstract class BaseTransactionProcessor {
|
|||
myModelConfig = theModelConfig;
|
||||
}
|
||||
|
||||
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||
/**
|
||||
* Searches for duplicate conditional creates and consolidates them.
|
||||
*
|
||||
* @param theEntries
|
||||
*/
|
||||
private void consolidateDuplicateConditionals(List<IBase> theEntries) {
|
||||
final HashMap<String, String> keyToUuid = new HashMap<>();
|
||||
for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) {
|
||||
IBase nextReqEntry = theEntries.get(index);
|
||||
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
|
||||
if (resource != null) {
|
||||
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
||||
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
|
||||
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
|
||||
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
||||
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
|
||||
|
||||
// Conditional UPDATE
|
||||
boolean consolidateEntry = false;
|
||||
if ("PUT".equals(verb)) {
|
||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
|
||||
int questionMarkIndex = requestUrl.indexOf('?');
|
||||
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
|
||||
consolidateEntry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional CREATE
|
||||
if ("POST".equals(verb)) {
|
||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
|
||||
if (!entryUrl.equals(requestUrl)) {
|
||||
consolidateEntry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (consolidateEntry) {
|
||||
if (!keyToUuid.containsKey(key)) {
|
||||
keyToUuid.put(key, entryUrl);
|
||||
} else {
|
||||
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
|
||||
theEntries.remove(index);
|
||||
index--;
|
||||
String existingUuid = keyToUuid.get(key);
|
||||
for (IBase nextEntry : theEntries) {
|
||||
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
|
||||
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
|
||||
// We're interested in any references directly to the placeholder ID, but also
|
||||
// references that have a resource target that has the placeholder ID.
|
||||
String nextReferenceId = nextReference.getReferenceElement().getValue();
|
||||
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
|
||||
nextReferenceId = nextReference.getResource().getIdElement().getValue();
|
||||
}
|
||||
if (entryUrl.equals(nextReferenceId)) {
|
||||
nextReference.setReference(existingUuid);
|
||||
nextReference.setResource(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next resource id (IIdType) from the base resource and next request entry.
|
||||
* @param theBaseResource - base resource
|
||||
* @param theNextReqEntry - next request entry
|
||||
* @param theAllIds - set of all IIdType values
|
||||
* @return
|
||||
*/
|
||||
private IIdType getNextResourceIdFromBaseResource(IBaseResource theBaseResource,
|
||||
IBase theNextReqEntry,
|
||||
Set<IIdType> theAllIds) {
|
||||
IIdType nextResourceId = null;
|
||||
if (theBaseResource != null) {
|
||||
nextResourceId = theBaseResource.getIdElement();
|
||||
|
||||
String fullUrl = myVersionAdapter.getFullUrl(theNextReqEntry);
|
||||
if (isNotBlank(fullUrl)) {
|
||||
IIdType fullUrlIdType = newIdType(fullUrl);
|
||||
if (isPlaceholder(fullUrlIdType)) {
|
||||
nextResourceId = fullUrlIdType;
|
||||
} else if (!nextResourceId.hasIdPart()) {
|
||||
nextResourceId = fullUrlIdType;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
|
||||
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
|
||||
nextResourceId = newIdType(toResourceName(theBaseResource.getClass()), nextResourceId.getIdPart());
|
||||
theBaseResource.setId(nextResourceId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
|
||||
*/
|
||||
if (isPlaceholder(nextResourceId)) {
|
||||
if (!theAllIds.add(nextResourceId)) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
|
||||
}
|
||||
} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
|
||||
IIdType nextId = nextResourceId.toUnqualifiedVersionless();
|
||||
if (!theAllIds.add(nextId)) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nextResourceId;
|
||||
}
|
||||
|
||||
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
|
||||
TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||
List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||
|
||||
theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts(
|
||||
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED,
|
||||
|
@ -673,7 +822,6 @@ public abstract class BaseTransactionProcessor {
|
|||
Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED
|
||||
);
|
||||
try {
|
||||
|
||||
Set<String> deletedResources = new HashSet<>();
|
||||
DeleteConflictList deleteConflicts = new DeleteConflictList();
|
||||
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
|
||||
|
@ -685,117 +833,20 @@ public abstract class BaseTransactionProcessor {
|
|||
/*
|
||||
* Look for duplicate conditional creates and consolidate them
|
||||
*/
|
||||
final HashMap<String, String> keyToUuid = new HashMap<>();
|
||||
for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) {
|
||||
IBase nextReqEntry = theEntries.get(index);
|
||||
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
|
||||
if (resource != null) {
|
||||
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
||||
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
|
||||
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
|
||||
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
|
||||
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
|
||||
|
||||
// Conditional UPDATE
|
||||
boolean consolidateEntry = false;
|
||||
if ("PUT".equals(verb)) {
|
||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
|
||||
int questionMarkIndex = requestUrl.indexOf('?');
|
||||
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
|
||||
consolidateEntry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional CREATE
|
||||
if ("POST".equals(verb)) {
|
||||
if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
|
||||
if (!entryUrl.equals(requestUrl)) {
|
||||
consolidateEntry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (consolidateEntry) {
|
||||
if (!keyToUuid.containsKey(key)) {
|
||||
keyToUuid.put(key, entryUrl);
|
||||
} else {
|
||||
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
|
||||
theEntries.remove(index);
|
||||
index--;
|
||||
String existingUuid = keyToUuid.get(key);
|
||||
for (IBase nextEntry : theEntries) {
|
||||
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
|
||||
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
|
||||
// We're interested in any references directly to the placeholder ID, but also
|
||||
// references that have a resource target that has the placeholder ID.
|
||||
String nextReferenceId = nextReference.getReferenceElement().getValue();
|
||||
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
|
||||
nextReferenceId = nextReference.getResource().getIdElement().getValue();
|
||||
}
|
||||
if (entryUrl.equals(nextReferenceId)) {
|
||||
nextReference.setReference(existingUuid);
|
||||
nextReference.setResource(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consolidateDuplicateConditionals(theEntries);
|
||||
|
||||
/*
|
||||
* Loop through the request and process any entries of type
|
||||
* PUT, POST or DELETE
|
||||
*/
|
||||
for (int i = 0; i < theEntries.size(); i++) {
|
||||
|
||||
if (i % 250 == 0) {
|
||||
ourLog.debug("Processed {} non-GET entries out of {} in transaction", i, theEntries.size());
|
||||
}
|
||||
|
||||
IBase nextReqEntry = theEntries.get(i);
|
||||
IBaseResource res = myVersionAdapter.getResource(nextReqEntry);
|
||||
IIdType nextResourceId = null;
|
||||
if (res != null) {
|
||||
|
||||
nextResourceId = res.getIdElement();
|
||||
|
||||
String fullUrl = myVersionAdapter.getFullUrl(nextReqEntry);
|
||||
if (isNotBlank(fullUrl)) {
|
||||
IIdType fullUrlIdType = newIdType(fullUrl);
|
||||
if (isPlaceholder(fullUrlIdType)) {
|
||||
nextResourceId = fullUrlIdType;
|
||||
} else if (!nextResourceId.hasIdPart()) {
|
||||
nextResourceId = fullUrlIdType;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
|
||||
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
|
||||
nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart());
|
||||
res.setId(nextResourceId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
|
||||
*/
|
||||
if (isPlaceholder(nextResourceId)) {
|
||||
if (!theAllIds.add(nextResourceId)) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
|
||||
}
|
||||
} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
|
||||
IIdType nextId = nextResourceId.toUnqualifiedVersionless();
|
||||
if (!theAllIds.add(nextId)) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
IIdType nextResourceId = getNextResourceIdFromBaseResource(res, nextReqEntry, theAllIds);
|
||||
|
||||
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
||||
String resourceType = res != null ? myContext.getResourceType(res) : null;
|
||||
|
@ -904,7 +955,8 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
|
||||
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId,
|
||||
outcome, nextRespEntry, resourceType, res, theRequest);
|
||||
entriesToProcess.put(nextRespEntry, outcome.getId());
|
||||
break;
|
||||
}
|
||||
|
@ -971,52 +1023,24 @@ public abstract class BaseTransactionProcessor {
|
|||
* was also deleted as a part of this transaction, which is why we check this now at the
|
||||
* end.
|
||||
*/
|
||||
for (Iterator<DeleteConflict> iter = deleteConflicts.iterator(); iter.hasNext(); ) {
|
||||
DeleteConflict nextDeleteConflict = iter.next();
|
||||
checkForDeleteConflicts(deleteConflicts, deletedResources, updatedResources);
|
||||
|
||||
/*
|
||||
* If we have a conflict, it means we can't delete Resource/A because
|
||||
* Resource/B has a reference to it. We'll ignore that conflict though
|
||||
* if it turns out we're also deleting Resource/B in this transaction.
|
||||
*/
|
||||
if (deletedResources.contains(nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) {
|
||||
iter.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* And then, this is kind of a last ditch check. It's also ok to delete
|
||||
* Resource/A if Resource/B isn't being deleted, but it is being UPDATED
|
||||
* in this transaction, and the updated version of it has no references
|
||||
* to Resource/A any more.
|
||||
*/
|
||||
String sourceId = nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue();
|
||||
String targetId = nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue();
|
||||
Optional<IBaseResource> updatedSource = updatedResources
|
||||
.stream()
|
||||
.filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
|
||||
.findFirst();
|
||||
if (updatedSource.isPresent()) {
|
||||
List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
|
||||
boolean sourceStillReferencesTarget = referencesInSource
|
||||
.stream()
|
||||
.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
|
||||
if (!sourceStillReferencesTarget) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(myContext, deleteConflicts);
|
||||
|
||||
theIdToPersistedOutcome.entrySet().forEach(t -> theTransactionDetails.addResolvedResourceId(t.getKey(), t.getValue().getPersistentId()));
|
||||
theIdToPersistedOutcome.entrySet().forEach(idAndOutcome -> {
|
||||
theTransactionDetails.addResolvedResourceId(idAndOutcome.getKey(), idAndOutcome.getValue().getPersistentId());
|
||||
});
|
||||
|
||||
/*
|
||||
* Perform ID substitutions and then index each resource we have saved
|
||||
*/
|
||||
|
||||
resolveReferencesThenSaveAndIndexResources(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, theTransactionStopWatch, entriesToProcess, nonUpdatedEntities, updatedEntities);
|
||||
resolveReferencesThenSaveAndIndexResources(theRequest, theTransactionDetails,
|
||||
theIdSubstitutions, theIdToPersistedOutcome,
|
||||
theTransactionStopWatch, entriesToProcess,
|
||||
nonUpdatedEntities, updatedEntities);
|
||||
|
||||
theTransactionStopWatch.endCurrentTask();
|
||||
|
||||
// flush writes to db
|
||||
theTransactionStopWatch.startTask("Flush writes to database");
|
||||
|
||||
flushSession(theIdToPersistedOutcome);
|
||||
|
@ -1070,6 +1094,53 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for any delete conflicts.
|
||||
* @param theDeleteConflicts - set of delete conflicts
|
||||
* @param theDeletedResources - set of deleted resources
|
||||
* @param theUpdatedResources - list of updated resources
|
||||
*/
|
||||
private void checkForDeleteConflicts(DeleteConflictList theDeleteConflicts,
|
||||
Set<String> theDeletedResources,
|
||||
List<IBaseResource> theUpdatedResources) {
|
||||
for (Iterator<DeleteConflict> iter = theDeleteConflicts.iterator(); iter.hasNext(); ) {
|
||||
DeleteConflict nextDeleteConflict = iter.next();
|
||||
|
||||
/*
|
||||
* If we have a conflict, it means we can't delete Resource/A because
|
||||
* Resource/B has a reference to it. We'll ignore that conflict though
|
||||
* if it turns out we're also deleting Resource/B in this transaction.
|
||||
*/
|
||||
if (theDeletedResources.contains(nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) {
|
||||
iter.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* And then, this is kind of a last ditch check. It's also ok to delete
|
||||
* Resource/A if Resource/B isn't being deleted, but it is being UPDATED
|
||||
* in this transaction, and the updated version of it has no references
|
||||
* to Resource/A any more.
|
||||
*/
|
||||
String sourceId = nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue();
|
||||
String targetId = nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue();
|
||||
Optional<IBaseResource> updatedSource = theUpdatedResources
|
||||
.stream()
|
||||
.filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
|
||||
.findFirst();
|
||||
if (updatedSource.isPresent()) {
|
||||
List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
|
||||
boolean sourceStillReferencesTarget = referencesInSource
|
||||
.stream()
|
||||
.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
|
||||
if (!sourceStillReferencesTarget) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(myContext, theDeleteConflicts);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method replaces any placeholder references in the
|
||||
* source transaction Bundle with their actual targets, then stores the resource contents and indexes
|
||||
|
@ -1092,7 +1163,10 @@ public abstract class BaseTransactionProcessor {
|
|||
* pass because it's too complex to try and insert the auto-versioned references and still
|
||||
* account for NOPs, so we block NOPs in that pass.
|
||||
*/
|
||||
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
|
||||
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess,
|
||||
Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
|
||||
FhirTerser terser = myContext.newTerser();
|
||||
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
|
||||
IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null;
|
||||
|
@ -1114,8 +1188,15 @@ public abstract class BaseTransactionProcessor {
|
|||
|
||||
Set<IBaseReference> referencesToAutoVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
|
||||
if (referencesToAutoVersion.isEmpty()) {
|
||||
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
|
||||
// no references to autoversion - we can do the resolve and save now
|
||||
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails,
|
||||
theIdSubstitutions, theIdToPersistedOutcome,
|
||||
entriesToProcess, nonUpdatedEntities,
|
||||
updatedEntities, terser,
|
||||
nextOutcome, nextResource,
|
||||
referencesToAutoVersion); // this is empty
|
||||
} else {
|
||||
// we have autoversioned things to defer until later
|
||||
if (deferredIndexesForAutoVersioning == null) {
|
||||
deferredIndexesForAutoVersioning = new IdentityHashMap<>();
|
||||
}
|
||||
|
@ -1129,12 +1210,24 @@ public abstract class BaseTransactionProcessor {
|
|||
DaoMethodOutcome nextOutcome = nextEntry.getKey();
|
||||
Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue();
|
||||
IBaseResource nextResource = nextOutcome.getResource();
|
||||
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
|
||||
|
||||
|
||||
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails,
|
||||
theIdSubstitutions, theIdToPersistedOutcome,
|
||||
entriesToProcess, nonUpdatedEntities,
|
||||
updatedEntities, terser,
|
||||
nextOutcome, nextResource,
|
||||
referencesToAutoVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities, FhirTerser terser, DaoMethodOutcome nextOutcome, IBaseResource nextResource, Set<IBaseReference> theReferencesToAutoVersion) {
|
||||
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities,
|
||||
Set<IBasePersistedResource> updatedEntities, FhirTerser terser,
|
||||
DaoMethodOutcome nextOutcome, IBaseResource nextResource,
|
||||
Set<IBaseReference> theReferencesToAutoVersion) {
|
||||
// References
|
||||
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
|
||||
for (ResourceReferenceInfo nextRef : allRefs) {
|
||||
|
@ -1175,9 +1268,34 @@ public abstract class BaseTransactionProcessor {
|
|||
} else if (nextId.getValue().startsWith("urn:")) {
|
||||
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
|
||||
} else {
|
||||
// get a map of
|
||||
// existing ids -> PID (for resources that exist in the DB)
|
||||
// should this be allPartitions?
|
||||
ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
|
||||
theReferencesToAutoVersion.stream()
|
||||
.map(IBaseReference::getReferenceElement).collect(Collectors.toList()));
|
||||
|
||||
for (IBaseReference baseRef : theReferencesToAutoVersion) {
|
||||
IIdType id = baseRef.getReferenceElement();
|
||||
if (!resourceVersionMap.containsKey(id)
|
||||
&& myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
|
||||
// not in the db, but autocreateplaceholders is true
|
||||
// so the version we'll set is "1" (since it will be
|
||||
// created later)
|
||||
String newRef = id.withVersion("1").getValue();
|
||||
id.setValue(newRef);
|
||||
} else {
|
||||
// we will add the looked up info to the transaction
|
||||
// for later
|
||||
theTransactionDetails.addResolvedResourceId(id,
|
||||
resourceVersionMap.getResourcePersistentId(id));
|
||||
}
|
||||
}
|
||||
|
||||
if (theReferencesToAutoVersion.contains(resourceReference)) {
|
||||
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
|
||||
if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
|
||||
|
||||
if (outcome != null && !outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
|
||||
addRollbackReferenceRestore(theTransactionDetails, resourceReference);
|
||||
resourceReference.setReference(nextId.getValue());
|
||||
resourceReference.setResource(null);
|
||||
|
@ -1565,59 +1683,81 @@ public abstract class BaseTransactionProcessor {
|
|||
return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
|
||||
}
|
||||
|
||||
public class BundleTask implements Runnable {
|
||||
public class RetriableBundleTask implements Runnable {
|
||||
|
||||
private CountDownLatch myCompletedLatch;
|
||||
private RequestDetails myRequestDetails;
|
||||
private IBase myNextReqEntry;
|
||||
private Map<Integer, Object> myResponseMap;
|
||||
private int myResponseOrder;
|
||||
private boolean myNestedMode;
|
||||
|
||||
protected BundleTask(CountDownLatch theCompletedLatch, RequestDetails theRequestDetails, Map<Integer, Object> theResponseMap, int theResponseOrder, IBase theNextReqEntry, boolean theNestedMode) {
|
||||
private final CountDownLatch myCompletedLatch;
|
||||
private final RequestDetails myRequestDetails;
|
||||
private final IBase myNextReqEntry;
|
||||
private final Map<Integer, Object> myResponseMap;
|
||||
private final int myResponseOrder;
|
||||
private final boolean myNestedMode;
|
||||
private BaseServerResponseException myLastSeenException;
|
||||
|
||||
protected RetriableBundleTask(CountDownLatch theCompletedLatch, RequestDetails theRequestDetails, Map<Integer, Object> theResponseMap, int theResponseOrder, IBase theNextReqEntry, boolean theNestedMode) {
|
||||
this.myCompletedLatch = theCompletedLatch;
|
||||
this.myRequestDetails = theRequestDetails;
|
||||
this.myNextReqEntry = theNextReqEntry;
|
||||
this.myResponseMap = theResponseMap;
|
||||
this.myResponseOrder = theResponseOrder;
|
||||
this.myResponseMap = theResponseMap;
|
||||
this.myResponseOrder = theResponseOrder;
|
||||
this.myNestedMode = theNestedMode;
|
||||
this.myLastSeenException = null;
|
||||
}
|
||||
|
||||
|
||||
private void processBatchEntry() {
|
||||
IBaseBundle subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode());
|
||||
myVersionAdapter.addEntry(subRequestBundle, myNextReqEntry);
|
||||
|
||||
IBaseBundle nextResponseBundle = processTransactionAsSubRequest(myRequestDetails, subRequestBundle, "Batch sub-request", myNestedMode);
|
||||
|
||||
IBase subResponseEntry = (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0);
|
||||
myResponseMap.put(myResponseOrder, subResponseEntry);
|
||||
|
||||
/*
|
||||
* If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it
|
||||
*/
|
||||
if (myVersionAdapter.getResource(subResponseEntry) == null) {
|
||||
IBase nextResponseBundleFirstEntry = (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0);
|
||||
myResponseMap.put(myResponseOrder, nextResponseBundleFirstEntry);
|
||||
}
|
||||
}
|
||||
private boolean processBatchEntryWithRetry() {
|
||||
int maxAttempts =3;
|
||||
for (int attempt = 1;; attempt++) {
|
||||
try {
|
||||
processBatchEntry();
|
||||
return true;
|
||||
} catch (BaseServerResponseException e) {
|
||||
//If we catch a known and structured exception from HAPI, just fail.
|
||||
myLastSeenException = e;
|
||||
return false;
|
||||
} catch (Throwable t) {
|
||||
myLastSeenException = new InternalErrorException(t);
|
||||
//If we have caught a non-tag-storage failure we are unfamiliar with, or we have exceeded max attempts, exit.
|
||||
if (!DaoFailureUtil.isTagStorageFailure(t) || attempt >= maxAttempts) {
|
||||
ourLog.error("Failure during BATCH sub transaction processing", t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
|
||||
try {
|
||||
IBaseBundle subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode());
|
||||
myVersionAdapter.addEntry(subRequestBundle, (IBase) myNextReqEntry);
|
||||
|
||||
IBaseBundle nextResponseBundle = processTransactionAsSubRequest(myRequestDetails, subRequestBundle, "Batch sub-request", myNestedMode);
|
||||
|
||||
IBase subResponseEntry = (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0);
|
||||
myResponseMap.put(myResponseOrder, subResponseEntry);
|
||||
|
||||
/*
|
||||
* If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it
|
||||
*/
|
||||
if (myVersionAdapter.getResource(subResponseEntry) == null) {
|
||||
IBase nextResponseBundleFirstEntry = (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0);
|
||||
myResponseMap.put(myResponseOrder, nextResponseBundleFirstEntry);
|
||||
}
|
||||
|
||||
} catch (BaseServerResponseException e) {
|
||||
caughtEx.setException(e);
|
||||
} catch (Throwable t) {
|
||||
ourLog.error("Failure during BATCH sub transaction processing", t);
|
||||
caughtEx.setException(new InternalErrorException(t));
|
||||
boolean success = processBatchEntryWithRetry();
|
||||
if (!success) {
|
||||
populateResponseMapWithLastSeenException();
|
||||
}
|
||||
|
||||
if (caughtEx.getException() != null) {
|
||||
// add exception to the response map
|
||||
myResponseMap.put(myResponseOrder, caughtEx);
|
||||
}
|
||||
|
||||
// checking for the parallelism
|
||||
ourLog.debug("processing bacth for {} is completed", myVersionAdapter.getEntryRequestUrl((IBase)myNextReqEntry));
|
||||
ourLog.debug("processing batch for {} is completed", myVersionAdapter.getEntryRequestUrl(myNextReqEntry));
|
||||
myCompletedLatch.countDown();
|
||||
}
|
||||
|
||||
private void populateResponseMapWithLastSeenException() {
|
||||
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
|
||||
caughtEx.setException(myLastSeenException);
|
||||
myResponseMap.put(myResponseOrder, caughtEx);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility class to help identify classes of failure.
|
||||
*/
|
||||
public class DaoFailureUtil {
|
||||
|
||||
public static boolean isTagStorageFailure(Throwable t) {
|
||||
if (StringUtils.isBlank(t.getMessage())) {
|
||||
return false;
|
||||
} else {
|
||||
String msg = t.getMessage().toLowerCase();
|
||||
return msg.contains("hfj_tag_def") || msg.contains("hfj_res_tag");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -100,6 +100,16 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
|
|||
@Query("SELECT t.myVersion FROM ResourceTable t WHERE t.myId = :pid")
|
||||
Long findCurrentVersionByPid(@Param("pid") Long thePid);
|
||||
|
||||
/**
|
||||
* This query will return rows with the following values:
|
||||
* Id (resource pid - long), ResourceType (Patient, etc), version (long)
|
||||
* Order matters!
|
||||
* @param pid - list of pids to get versions for
|
||||
* @return
|
||||
*/
|
||||
@Query("SELECT t.myId, t.myResourceType, t.myVersion FROM ResourceTable t WHERE t.myId IN ( :pid )")
|
||||
Collection<Object[]> getResourceVersionsForPid(@Param("pid") List<Long> pid);
|
||||
|
||||
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId IS NULL AND t.myId = :pid")
|
||||
Optional<ResourceTable> readByPartitionIdNull(@Param("pid") Long theResourceId);
|
||||
|
||||
|
|
|
@ -94,7 +94,6 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
|
||||
|
||||
}
|
||||
|
||||
resolvedResource = createdTableOpt.get();
|
||||
}
|
||||
|
||||
|
|
|
@ -309,7 +309,7 @@ public class IdHelperService {
|
|||
if (forcedId.isPresent()) {
|
||||
retVal.setValue(theResourceType + '/' + forcedId.get());
|
||||
} else {
|
||||
retVal.setValue(theResourceType + '/' + theId.toString());
|
||||
retVal.setValue(theResourceType + '/' + theId);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
|||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy;
|
||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.dao.DaoFailureUtil;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
|
@ -93,10 +94,9 @@ public class HapiTransactionService {
|
|||
* known to the system already, they'll both try to create a row in HFJ_TAG_DEF,
|
||||
* which is the tag definition table. In that case, a constraint error will be
|
||||
* thrown by one of the client threads, so we auto-retry in order to avoid
|
||||
* annopying spurious failures for the client.
|
||||
* annoying spurious failures for the client.
|
||||
*/
|
||||
if (e.getMessage().contains("HFJ_TAG_DEF") || e.getMessage().contains("hfj_tag_def") ||
|
||||
e.getMessage().contains("HFJ_RES_TAG") || e.getMessage().contains("hfj_res_tag")) {
|
||||
if (DaoFailureUtil.isTagStorageFailure(e)) {
|
||||
maxRetries = 3;
|
||||
}
|
||||
|
||||
|
|
|
@ -1087,7 +1087,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
ourLog.trace("Performing count");
|
||||
ISearchBuilder sb = newSearchBuilder();
|
||||
Iterator<Long> countIterator = sb.createCountQuery(myParams, mySearch.getUuid(), myRequest, myRequestPartitionId);
|
||||
Long count = countIterator.hasNext() ? countIterator.next() : 0;
|
||||
Long count = countIterator.hasNext() ? countIterator.next() : 0L;
|
||||
ourLog.trace("Got count {}", count);
|
||||
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
|
||||
|
|
|
@ -729,10 +729,10 @@ public class QueryStack {
|
|||
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theList, theOperation, theRequestPartitionId);
|
||||
}
|
||||
|
||||
private Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
||||
String theResourceName, String theParamName, RuntimeSearchParam theSearchParam,
|
||||
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
|
||||
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
||||
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
||||
String theResourceName, String theParamName, RuntimeSearchParam theSearchParam,
|
||||
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
|
||||
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
||||
|
||||
String spnamePrefix = theParamName;
|
||||
|
||||
|
@ -794,31 +794,31 @@ public class QueryStack {
|
|||
|
||||
switch (targetParamDefinition.getParamType()) {
|
||||
case DATE:
|
||||
containedCondition = createPredicateDate(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequestPartitionId);
|
||||
break;
|
||||
case NUMBER:
|
||||
containedCondition = createPredicateNumber(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequestPartitionId);
|
||||
break;
|
||||
case QUANTITY:
|
||||
containedCondition = createPredicateQuantity(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequestPartitionId);
|
||||
break;
|
||||
case STRING:
|
||||
containedCondition = createPredicateString(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequestPartitionId);
|
||||
break;
|
||||
case TOKEN:
|
||||
containedCondition = createPredicateToken(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequestPartitionId);
|
||||
break;
|
||||
case COMPOSITE:
|
||||
containedCondition = createPredicateComposite(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theRequestPartitionId);
|
||||
break;
|
||||
case URI:
|
||||
containedCondition = createPredicateUri(null, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequest, theRequestPartitionId);
|
||||
break;
|
||||
case HAS:
|
||||
|
@ -1162,10 +1162,24 @@ public class QueryStack {
|
|||
break;
|
||||
case REFERENCE:
|
||||
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
|
||||
if (theSearchContainedMode.equals(SearchContainedModeEnum.TRUE))
|
||||
andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
else
|
||||
if (theSearchContainedMode.equals(SearchContainedModeEnum.TRUE)) {
|
||||
// TODO: The _contained parameter is not intended to control search chain interpretation like this.
|
||||
// See SMILE-2898 for details.
|
||||
// For now, leave the incorrect implementation alone, just in case someone is relying on it,
|
||||
// until the complete fix is available.
|
||||
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
} else if (isEligibleForContainedResourceSearch(nextAnd)) {
|
||||
// TODO for now, restrict contained reference traversal to the last reference in the chain
|
||||
// We don't seem to be indexing the outbound references of a contained resource, so we can't
|
||||
// include them in search chains.
|
||||
// It would be nice to eventually relax this constraint, but no client seems to be asking for it.
|
||||
andPredicates.add(toOrPredicate(
|
||||
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId),
|
||||
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
|
||||
));
|
||||
} else {
|
||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STRING:
|
||||
|
@ -1243,6 +1257,14 @@ public class QueryStack {
|
|||
return toAndPredicate(andPredicates);
|
||||
}
|
||||
|
||||
private boolean isEligibleForContainedResourceSearch(List<? extends IQueryParameterType> nextAnd) {
|
||||
return myModelConfig.isIndexOnContainedResources() &&
|
||||
nextAnd.stream()
|
||||
.filter(t -> t instanceof ReferenceParam)
|
||||
.map(t -> (ReferenceParam) t)
|
||||
.noneMatch(t -> t.getChain().contains("."));
|
||||
}
|
||||
|
||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
|
||||
Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString);
|
||||
|
|
|
@ -377,7 +377,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
SearchQueryBuilder sqlBuilder = new SearchQueryBuilder(myContext, myDaoConfig.getModelConfig(), myPartitionSettings, myRequestPartitionId, sqlBuilderResourceName, mySqlBuilderFactory, myDialectProvider, theCount);
|
||||
QueryStack queryStack3 = new QueryStack(theParams, myDaoConfig, myDaoConfig.getModelConfig(), myContext, sqlBuilder, mySearchParamRegistry, myPartitionSettings);
|
||||
|
||||
if (theParams.keySet().size() > 1 || theParams.getSort() != null || theParams.keySet().contains(Constants.PARAM_HAS)) {
|
||||
if (theParams.keySet().size() > 1 || theParams.getSort() != null || theParams.keySet().contains(Constants.PARAM_HAS) || isPotentiallyContainedReferenceParameterExistsAtRoot(theParams)) {
|
||||
List<RuntimeSearchParam> activeComboParams = mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet());
|
||||
if (activeComboParams.isEmpty()) {
|
||||
sqlBuilder.setNeedResourceTableRoot(true);
|
||||
|
@ -487,6 +487,13 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
return Optional.of(executor);
|
||||
}
|
||||
|
||||
private boolean isPotentiallyContainedReferenceParameterExistsAtRoot(SearchParameterMap theParams) {
|
||||
return myModelConfig.isIndexOnContainedResources() && theParams.values().stream()
|
||||
.flatMap(Collection::stream)
|
||||
.flatMap(Collection::stream)
|
||||
.anyMatch(t -> t instanceof ReferenceParam);
|
||||
}
|
||||
|
||||
private List<Long> normalizeIdListForLastNInClause(List<Long> lastnResourceIds) {
|
||||
/*
|
||||
The following is a workaround to a known issue involving Hibernate. If queries are used with "in" clauses with large and varying
|
||||
|
|
|
@ -38,20 +38,18 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
|
|||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference;
|
||||
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
|
||||
import ca.uhn.fhir.jpa.search.builder.QueryStack;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.search.builder.QueryStack;
|
||||
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.param.CompositeParam;
|
||||
|
@ -66,6 +64,8 @@ import ca.uhn.fhir.rest.param.TokenParamModifier;
|
|||
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.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
||||
import com.healthmarketscience.sqlbuilder.ComboCondition;
|
||||
|
@ -341,15 +341,26 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
List<Condition> orPredicates = new ArrayList<>();
|
||||
boolean paramInverted = false;
|
||||
QueryStack childQueryFactory = myQueryStack.newChildQueryFactoryWithFullBuilderReuse();
|
||||
for (String nextType : resourceTypes) {
|
||||
String chain = theReferenceParam.getChain();
|
||||
|
||||
String remainingChain = null;
|
||||
int chainDotIndex = chain.indexOf('.');
|
||||
if (chainDotIndex != -1) {
|
||||
remainingChain = chain.substring(chainDotIndex + 1);
|
||||
chain = chain.substring(0, chainDotIndex);
|
||||
}
|
||||
String chain = theReferenceParam.getChain();
|
||||
|
||||
String remainingChain = null;
|
||||
int chainDotIndex = chain.indexOf('.');
|
||||
if (chainDotIndex != -1) {
|
||||
remainingChain = chain.substring(chainDotIndex + 1);
|
||||
chain = chain.substring(0, chainDotIndex);
|
||||
}
|
||||
|
||||
int qualifierIndex = chain.indexOf(':');
|
||||
String qualifier = null;
|
||||
if (qualifierIndex != -1) {
|
||||
qualifier = chain.substring(qualifierIndex);
|
||||
chain = chain.substring(0, qualifierIndex);
|
||||
}
|
||||
|
||||
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
|
||||
|
||||
for (String nextType : resourceTypes) {
|
||||
|
||||
RuntimeResourceDefinition typeDef = getFhirContext().getResourceDefinition(nextType);
|
||||
String subResourceName = typeDef.getName();
|
||||
|
@ -360,14 +371,6 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
continue;
|
||||
}
|
||||
|
||||
int qualifierIndex = chain.indexOf(':');
|
||||
String qualifier = null;
|
||||
if (qualifierIndex != -1) {
|
||||
qualifier = chain.substring(qualifierIndex);
|
||||
chain = chain.substring(0, qualifierIndex);
|
||||
}
|
||||
|
||||
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
|
||||
RuntimeSearchParam param = null;
|
||||
if (!isMeta) {
|
||||
param = mySearchParamRegistry.getActiveSearchParam(nextType, chain);
|
||||
|
@ -408,7 +411,6 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE));
|
||||
|
||||
orPredicates.add(toAndPredicate(andPredicates));
|
||||
|
||||
}
|
||||
|
||||
if (candidateTargetTypes.isEmpty()) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings
|
|||
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Properties;
|
||||
|
@ -49,13 +50,13 @@ import static org.slf4j.LoggerFactory.getLogger;
|
|||
* FHIR JPA server. This class also injects a starter template into the ES cluster.
|
||||
*/
|
||||
public class ElasticsearchHibernatePropertiesBuilder {
|
||||
private static final Logger ourLog = getLogger(ElasticsearchHibernatePropertiesBuilder.class);
|
||||
private static final Logger ourLog = getLogger(ElasticsearchHibernatePropertiesBuilder.class);
|
||||
|
||||
|
||||
private IndexStatus myRequiredIndexStatus = IndexStatus.YELLOW.YELLOW;
|
||||
private IndexStatus myRequiredIndexStatus = IndexStatus.YELLOW;
|
||||
private SchemaManagementStrategyName myIndexSchemaManagementStrategy = SchemaManagementStrategyName.CREATE;
|
||||
|
||||
private String myRestUrl;
|
||||
private String myHosts;
|
||||
private String myUsername;
|
||||
private String myPassword;
|
||||
private long myIndexManagementWaitTimeoutMillis = 10000L;
|
||||
|
@ -77,11 +78,8 @@ public class ElasticsearchHibernatePropertiesBuilder {
|
|||
|
||||
// the below properties are used for ElasticSearch integration
|
||||
theProperties.put(BackendSettings.backendKey(BackendSettings.TYPE), "elasticsearch");
|
||||
|
||||
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.ANALYSIS_CONFIGURER), HapiElasticsearchAnalysisConfigurer.class.getName());
|
||||
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.HOSTS), myRestUrl);
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.HOSTS), myHosts);
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.PROTOCOL), myProtocol);
|
||||
|
||||
if (StringUtils.isNotBlank(myUsername)) {
|
||||
|
@ -99,8 +97,10 @@ public class ElasticsearchHibernatePropertiesBuilder {
|
|||
theProperties.put(HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY, myDebugSyncStrategy);
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LOG_JSON_PRETTY_PRINTING), Boolean.toString(myDebugPrettyPrintJsonLog));
|
||||
|
||||
injectStartupTemplate(myProtocol, myRestUrl, myUsername, myPassword);
|
||||
//This tells elasticsearch to use our custom index naming strategy.
|
||||
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LAYOUT_STRATEGY), IndexNamePrefixLayoutStrategy.class.getName());
|
||||
|
||||
injectStartupTemplate(myProtocol, myHosts, myUsername, myPassword);
|
||||
}
|
||||
|
||||
public ElasticsearchHibernatePropertiesBuilder setRequiredIndexStatus(IndexStatus theRequiredIndexStatus) {
|
||||
|
@ -108,11 +108,8 @@ public class ElasticsearchHibernatePropertiesBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ElasticsearchHibernatePropertiesBuilder setRestUrl(String theRestUrl) {
|
||||
if (theRestUrl.contains("://")) {
|
||||
throw new ConfigurationException("Elasticsearch URL cannot include a protocol, that is a separate property. Remove http:// or https:// from this URL.");
|
||||
}
|
||||
myRestUrl = theRestUrl;
|
||||
public ElasticsearchHibernatePropertiesBuilder setHosts(String hosts) {
|
||||
myHosts = hosts;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -147,18 +144,13 @@ public class ElasticsearchHibernatePropertiesBuilder {
|
|||
* TODO GGG HS: In HS6.1, we should have a native way of performing index settings manipulation at bootstrap time, so this should
|
||||
* eventually be removed in favour of whatever solution they come up with.
|
||||
*/
|
||||
void injectStartupTemplate(String theProtocol, String theHostAndPort, String theUsername, String thePassword) {
|
||||
void injectStartupTemplate(String theProtocol, String theHosts, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
PutIndexTemplateRequest ngramTemplate = new PutIndexTemplateRequest("ngram-template")
|
||||
.patterns(Arrays.asList("resourcetable-*", "termconcept-*"))
|
||||
.patterns(Arrays.asList("*resourcetable-*", "*termconcept-*"))
|
||||
.settings(Settings.builder().put("index.max_ngram_diff", 50));
|
||||
|
||||
int colonIndex = theHostAndPort.indexOf(":");
|
||||
String host = theHostAndPort.substring(0, colonIndex);
|
||||
Integer port = Integer.valueOf(theHostAndPort.substring(colonIndex + 1));
|
||||
String qualifiedHost = theProtocol + "://" + host;
|
||||
|
||||
try {
|
||||
RestHighLevelClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(qualifiedHost, port, theUsername, thePassword);
|
||||
RestHighLevelClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(theProtocol, theHosts, theUsername, thePassword);
|
||||
ourLog.info("Adding starter template for large ngram diffs");
|
||||
AcknowledgedResponse acknowledgedResponse = elasticsearchHighLevelRestClient.indices().putTemplate(ngramTemplate, RequestOptions.DEFAULT);
|
||||
assert acknowledgedResponse.isAcknowledged();
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package ca.uhn.fhir.jpa.search.elastic;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy;
|
||||
import org.hibernate.search.backend.elasticsearch.logging.impl.Log;
|
||||
import org.hibernate.search.util.common.logging.impl.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This class instructs hibernate search on how to create index names for indexed entities.
|
||||
* In our case, we use this class to add an optional prefix to all indices which are created, which can be controlled via
|
||||
* {@link DaoConfig#setElasticSearchIndexPrefix(String)}.
|
||||
*/
|
||||
@Service
|
||||
public class IndexNamePrefixLayoutStrategy implements IndexLayoutStrategy {
|
||||
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
static final Log log = LoggerFactory.make(Log.class, MethodHandles.lookup());
|
||||
public static final String NAME = "prefix";
|
||||
public static final Pattern UNIQUE_KEY_EXTRACTION_PATTERN = Pattern.compile("(.*)-\\d{6}");
|
||||
|
||||
public String createInitialElasticsearchIndexName(String hibernateSearchIndexName) {
|
||||
return addPrefixIfNecessary(hibernateSearchIndexName + "-000001");
|
||||
}
|
||||
|
||||
public String createWriteAlias(String hibernateSearchIndexName) {
|
||||
return addPrefixIfNecessary(hibernateSearchIndexName +"-write");
|
||||
}
|
||||
|
||||
public String createReadAlias(String hibernateSearchIndexName) {
|
||||
return addPrefixIfNecessary(hibernateSearchIndexName + "-read");
|
||||
}
|
||||
|
||||
private String addPrefixIfNecessary(String theCandidateName) {
|
||||
validateDaoConfigIsPresent();
|
||||
if (!StringUtils.isBlank(myDaoConfig.getElasticSearchIndexPrefix())) {
|
||||
return myDaoConfig.getElasticSearchIndexPrefix() + "-" + theCandidateName;
|
||||
} else {
|
||||
return theCandidateName;
|
||||
}
|
||||
}
|
||||
|
||||
public String extractUniqueKeyFromHibernateSearchIndexName(String hibernateSearchIndexName) {
|
||||
return hibernateSearchIndexName;
|
||||
}
|
||||
|
||||
public String extractUniqueKeyFromElasticsearchIndexName(String elasticsearchIndexName) {
|
||||
Matcher matcher = UNIQUE_KEY_EXTRACTION_PATTERN.matcher(elasticsearchIndexName);
|
||||
if (!matcher.matches()) {
|
||||
throw log.invalidIndexPrimaryName(elasticsearchIndexName, UNIQUE_KEY_EXTRACTION_PATTERN);
|
||||
} else {
|
||||
String candidateUniqueKey= matcher.group(1);
|
||||
return removePrefixIfNecessary(candidateUniqueKey);
|
||||
}
|
||||
}
|
||||
|
||||
private String removePrefixIfNecessary(String theCandidateUniqueKey) {
|
||||
validateDaoConfigIsPresent();
|
||||
if (!StringUtils.isBlank(myDaoConfig.getElasticSearchIndexPrefix())) {
|
||||
return theCandidateUniqueKey.replace(myDaoConfig.getElasticSearchIndexPrefix() + "-", "");
|
||||
} else {
|
||||
return theCandidateUniqueKey;
|
||||
}
|
||||
}
|
||||
private void validateDaoConfigIsPresent() {
|
||||
if (myDaoConfig == null) {
|
||||
throw new ConfigurationException("While attempting to boot HAPI FHIR, the Hibernate Search bootstrapper failed to find the DaoConfig. This probably means Hibernate Search has been recently upgraded, or somebody modified HapiFhirLocalContainerEntityManagerFactoryBean.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.search.lastn;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
|
@ -27,45 +29,51 @@ import org.apache.http.auth.UsernamePasswordCredentials;
|
|||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.elasticsearch.client.Node;
|
||||
import org.elasticsearch.client.RestClient;
|
||||
import org.elasticsearch.client.RestClientBuilder;
|
||||
import org.elasticsearch.client.RestHighLevelClient;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ElasticsearchRestClientFactory {
|
||||
|
||||
|
||||
private static String determineScheme(String theHostname) {
|
||||
int schemeIdx = theHostname.indexOf("://");
|
||||
if (schemeIdx > 0) {
|
||||
return theHostname.substring(0, schemeIdx);
|
||||
} else {
|
||||
return "http";
|
||||
static public RestHighLevelClient createElasticsearchHighLevelRestClient(
|
||||
String protocol, String hosts, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
|
||||
if (hosts.contains("://")) {
|
||||
throw new ConfigurationException("Elasticsearch URLs cannot include a protocol, that is a separate property. Remove http:// or https:// from this URL.");
|
||||
}
|
||||
}
|
||||
|
||||
private static String stripHostOfScheme(String theHostname) {
|
||||
int schemeIdx = theHostname.indexOf("://");
|
||||
if (schemeIdx > 0) {
|
||||
return theHostname.substring(schemeIdx + 3);
|
||||
} else {
|
||||
return theHostname;
|
||||
String[] hostArray = hosts.split(",");
|
||||
List<Node> clientNodes = Arrays.stream(hostArray)
|
||||
.map(String::trim)
|
||||
.filter(s -> s.contains(":"))
|
||||
.map(h -> {
|
||||
int colonIndex = h.indexOf(":");
|
||||
String host = h.substring(0, colonIndex);
|
||||
int port = Integer.parseInt(h.substring(colonIndex + 1));
|
||||
return new Node(new HttpHost(host, port, protocol));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
if (hostArray.length != clientNodes.size()) {
|
||||
throw new ConfigurationException("Elasticsearch URLs have to contain ':' as a host:port separator. Example: localhost:9200,localhost:9201,localhost:9202");
|
||||
}
|
||||
|
||||
RestClientBuilder clientBuilder = RestClient.builder(clientNodes.toArray(new Node[0]));
|
||||
if (StringUtils.isNotBlank(theUsername) && StringUtils.isNotBlank(thePassword)) {
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(theUsername, thePassword));
|
||||
clientBuilder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
|
||||
.setDefaultCredentialsProvider(credentialsProvider));
|
||||
}
|
||||
Header[] defaultHeaders = new Header[]{new BasicHeader("Content-Type", "application/json")};
|
||||
clientBuilder.setDefaultHeaders(defaultHeaders);
|
||||
|
||||
return new RestHighLevelClient(clientBuilder);
|
||||
|
||||
}
|
||||
|
||||
static public RestHighLevelClient createElasticsearchHighLevelRestClient(String theHostname, int thePort, String theUsername, String thePassword) {
|
||||
final CredentialsProvider credentialsProvider =
|
||||
new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(theUsername, thePassword));
|
||||
RestClientBuilder clientBuilder = RestClient.builder(
|
||||
new HttpHost(stripHostOfScheme(theHostname), thePort, determineScheme(theHostname)))
|
||||
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
|
||||
.setDefaultCredentialsProvider(credentialsProvider));
|
||||
|
||||
Header[] defaultHeaders = new Header[]{new BasicHeader("Content-Type", "application/json")};
|
||||
clientBuilder.setDefaultHeaders(defaultHeaders);
|
||||
|
||||
return new RestHighLevelClient(clientBuilder);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,11 +68,11 @@ import org.elasticsearch.search.aggregations.bucket.terms.ParsedTerms;
|
|||
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
|
||||
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
|
||||
import org.elasticsearch.search.aggregations.metrics.ParsedTopHits;
|
||||
import org.elasticsearch.search.aggregations.support.ValueType;
|
||||
import org.elasticsearch.search.builder.SearchSourceBuilder;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
@ -125,13 +125,13 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
|
|||
private PartitionSettings myPartitionSettings;
|
||||
|
||||
//This constructor used to inject a dummy partitionsettings in test.
|
||||
public ElasticsearchSvcImpl(PartitionSettings thePartitionSetings, String theHostname, int thePort, String theUsername, String thePassword) {
|
||||
this(theHostname, thePort, theUsername, thePassword);
|
||||
public ElasticsearchSvcImpl(PartitionSettings thePartitionSetings, String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
this(theHostname, theUsername, thePassword);
|
||||
this.myPartitionSettings = thePartitionSetings;
|
||||
}
|
||||
|
||||
public ElasticsearchSvcImpl(String theHostname, int thePort, String theUsername, String thePassword) {
|
||||
myRestHighLevelClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(theHostname, thePort, theUsername, thePassword);
|
||||
public ElasticsearchSvcImpl(String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
myRestHighLevelClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient("http", theHostname, theUsername, thePassword);
|
||||
|
||||
try {
|
||||
createObservationIndexIfMissing();
|
||||
|
|
|
@ -126,5 +126,8 @@ public class SqlQuery {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getSql(true, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,12 +21,12 @@ public class ResourceVersionCacheSvcTest extends BaseJpaR4Test {
|
|||
IIdType patientId = myPatientDao.create(patient).getId();
|
||||
ResourceVersionMap versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
|
||||
assertEquals(1, versionMap.size());
|
||||
assertEquals("1", versionMap.getVersion(patientId));
|
||||
assertEquals(1L, versionMap.getVersion(patientId));
|
||||
|
||||
patient.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
myPatientDao.update(patient);
|
||||
versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
|
||||
assertEquals(1, versionMap.size());
|
||||
assertEquals("2", versionMap.getVersion(patientId));
|
||||
assertEquals(2L, versionMap.getVersion(patientId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.config.BlockLargeNumbersOfParamsListener;
|
||||
import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
|
||||
import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
|
||||
import ca.uhn.fhir.jpa.dao.r4.ElasticsearchPrefixTest;
|
||||
import ca.uhn.fhir.jpa.search.elastic.HapiElasticsearchAnalysisConfigurer;
|
||||
import ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy;
|
||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchRestClientFactory;
|
||||
import ca.uhn.fhir.jpa.search.lastn.config.TestElasticsearchContainerHelper;
|
||||
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||
import org.apache.commons.dbcp2.BasicDataSource;
|
||||
import org.elasticsearch.action.support.master.AcknowledgedResponse;
|
||||
import org.elasticsearch.client.RequestOptions;
|
||||
import org.elasticsearch.client.RestHighLevelClient;
|
||||
import org.elasticsearch.client.indices.PutIndexTemplateRequest;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.hibernate.dialect.H2Dialect;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings;
|
||||
import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings;
|
||||
import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
|
||||
import org.hibernate.search.engine.cfg.BackendSettings;
|
||||
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
||||
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The only reason this is its own class is so that we can set a dao config setting before the whole test framework comes online.
|
||||
* We need to do this as it is during bean creation that HS bootstrapping occurs.
|
||||
*/
|
||||
@Configuration
|
||||
public class ElasticsearchWithPrefixConfig {
|
||||
|
||||
@Bean
|
||||
public DaoConfig daoConfig() {
|
||||
DaoConfig daoConfig = new DaoConfig();
|
||||
daoConfig.setElasticSearchIndexPrefix(ElasticsearchPrefixTest.ELASTIC_PREFIX);
|
||||
return daoConfig;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IndexNamePrefixLayoutStrategy indexNamePrefixLayoutStrategy() {
|
||||
return new IndexNamePrefixLayoutStrategy();
|
||||
}
|
||||
@Bean
|
||||
public FhirContext fhirContext() {
|
||||
return FhirContext.forR4();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = new HapiFhirLocalContainerEntityManagerFactoryBean(theConfigurableListableBeanFactory);
|
||||
retVal.setJpaDialect(new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer()));
|
||||
retVal.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");
|
||||
retVal.setPersistenceProvider(new HibernatePersistenceProvider());
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaR4");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
return retVal;
|
||||
}
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
BasicDataSource retVal = new BasicDataSource();
|
||||
retVal.setDriver(new org.h2.Driver());
|
||||
retVal.setUrl("jdbc:h2:mem:testdb_r4");
|
||||
retVal.setMaxWaitMillis(30000);
|
||||
retVal.setUsername("");
|
||||
retVal.setPassword("");
|
||||
retVal.setMaxTotal(5);
|
||||
|
||||
SLF4JLogLevel level = SLF4JLogLevel.INFO;
|
||||
DataSource dataSource = ProxyDataSourceBuilder
|
||||
.create(retVal)
|
||||
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS, level)
|
||||
.beforeQuery(new BlockLargeNumbersOfParamsListener())
|
||||
.afterQuery(new CurrentThreadCaptureQueriesListener())
|
||||
.build();
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Properties jpaProperties() {
|
||||
Properties extraProperties = new Properties();
|
||||
extraProperties.put("hibernate.format_sql", "false");
|
||||
extraProperties.put("hibernate.show_sql", "false");
|
||||
extraProperties.put("hibernate.hbm2ddl.auto", "update");
|
||||
extraProperties.put("hibernate.dialect", H2Dialect.class.getName());
|
||||
//Override default lucene settings
|
||||
// Force elasticsearch to start first
|
||||
int httpPort = elasticContainer().getMappedPort(9200);//9200 is the HTTP port
|
||||
String host = elasticContainer().getHost();
|
||||
// the below properties are used for ElasticSearch integration
|
||||
extraProperties.put(BackendSettings.backendKey(BackendSettings.TYPE), "elasticsearch");
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.ANALYSIS_CONFIGURER), HapiElasticsearchAnalysisConfigurer.class.getName());
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.HOSTS), host + ":" + httpPort);
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.PROTOCOL), "http");
|
||||
extraProperties.put(HibernateOrmMapperSettings.SCHEMA_MANAGEMENT_STRATEGY, SchemaManagementStrategyName.CREATE.externalRepresentation());
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_MINIMAL_REQUIRED_STATUS_WAIT_TIMEOUT), Long.toString(10000));
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_MINIMAL_REQUIRED_STATUS), IndexStatus.YELLOW.externalRepresentation());
|
||||
// Need the mapping to be dynamic because of terminology indexes.
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.DYNAMIC_MAPPING), "true");
|
||||
// Only for unit tests
|
||||
extraProperties.put(HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY, "read-sync");
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LOG_JSON_PRETTY_PRINTING), Boolean.toString(true));
|
||||
|
||||
//This tells elasticsearch to use our custom index naming strategy.
|
||||
extraProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LAYOUT_STRATEGY), IndexNamePrefixLayoutStrategy.class.getName());
|
||||
|
||||
PutIndexTemplateRequest ngramTemplate = new PutIndexTemplateRequest("ngram-template")
|
||||
.patterns(Arrays.asList("*resourcetable-*", "*termconcept-*"))
|
||||
.settings(Settings.builder().put("index.max_ngram_diff", 50));
|
||||
|
||||
try {
|
||||
RestHighLevelClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient("http", host + ":" + httpPort, "", "");
|
||||
AcknowledgedResponse acknowledgedResponse = elasticsearchHighLevelRestClient.indices().putTemplate(ngramTemplate, RequestOptions.DEFAULT);
|
||||
assert acknowledgedResponse.isAcknowledged();
|
||||
} catch (IOException theE) {
|
||||
theE.printStackTrace();
|
||||
throw new ConfigurationException("Couldn't connect to the elasticsearch server to create necessary templates. Ensure the Elasticsearch user has permissions to create templates.");
|
||||
}
|
||||
return extraProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ElasticsearchContainer elasticContainer() {
|
||||
ElasticsearchContainer embeddedElasticSearch = TestElasticsearchContainerHelper.getEmbeddedElasticSearch();
|
||||
embeddedElasticSearch.start();
|
||||
return embeddedElasticSearch;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import org.hibernate.search.engine.cfg.BackendSettings;
|
|||
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -134,8 +135,8 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu2");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
|
|||
import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
|
||||
import org.hibernate.search.engine.cfg.BackendSettings;
|
||||
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -138,8 +139,8 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -3,12 +3,14 @@ package ca.uhn.fhir.jpa.config;
|
|||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.match.deliver.resthook.SubscriptionDeliveringRestHookSubscriber;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
||||
import ca.uhn.fhir.test.utilities.BatchJobHelper;
|
||||
import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy;
|
||||
import org.springframework.batch.core.explore.JobExplorer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.batch.BatchJobsConfig;
|
||||
import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter;
|
||||
import ca.uhn.fhir.jpa.batch.svc.BatchJobSubmitterImpl;
|
||||
|
@ -14,6 +15,8 @@ import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
|||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||
import org.apache.commons.dbcp2.BasicDataSource;
|
||||
import org.hibernate.dialect.H2Dialect;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -138,10 +141,15 @@ public class TestR4Config extends BaseJavaConfigR4 {
|
|||
return new SingleQueryCountHolder();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = new HapiFhirLocalContainerEntityManagerFactoryBean(theConfigurableListableBeanFactory);
|
||||
configureEntityManagerFactory(retVal, fhirContext());
|
||||
retVal.setJpaDialect(new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer()));
|
||||
retVal.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");
|
||||
retVal.setPersistenceProvider(new HibernatePersistenceProvider());
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaR4");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
|
||||
import ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy;
|
||||
import ca.uhn.fhir.jpa.search.lastn.config.TestElasticsearchContainerHelper;
|
||||
import org.h2.index.Index;
|
||||
import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
|
||||
import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy;
|
||||
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
@ -35,7 +41,7 @@ public class TestR4ConfigWithElasticSearch extends TestR4Config {
|
|||
.setIndexSchemaManagementStrategy(SchemaManagementStrategyName.CREATE)
|
||||
.setIndexManagementWaitTimeoutMillis(10000)
|
||||
.setRequiredIndexStatus(IndexStatus.YELLOW)
|
||||
.setRestUrl(host+ ":" + httpPort)
|
||||
.setHosts(host + ":" + httpPort)
|
||||
.setProtocol("http")
|
||||
.setUsername("")
|
||||
.setPassword("")
|
||||
|
|
|
@ -21,7 +21,7 @@ public class TestR4ConfigWithElasticsearchClient extends TestR4ConfigWithElastic
|
|||
public ElasticsearchSvcImpl myElasticsearchSvc() {
|
||||
int elasticsearchPort = elasticContainer().getMappedPort(9200);
|
||||
String host = elasticContainer().getHost();
|
||||
return new ElasticsearchSvcImpl(host, elasticsearchPort, "", "");
|
||||
return new ElasticsearchSvcImpl(host + ":" + elasticsearchPort, null, null);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
|
|
|
@ -3,9 +3,11 @@ package ca.uhn.fhir.jpa.config;
|
|||
import java.util.Properties;
|
||||
|
||||
import org.hibernate.dialect.H2Dialect;
|
||||
import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy;
|
||||
import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
|
||||
import org.hibernate.search.engine.cfg.BackendSettings;
|
||||
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
|
@ -27,8 +29,8 @@ public class TestR4WithLuceneDisabledConfig extends TestR4Config {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
return retVal;
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
|
|||
import org.hibernate.search.engine.cfg.BackendSettings;
|
||||
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -138,8 +139,8 @@ public class TestR5Config extends BaseJavaConfigR5 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaR5");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class BaseHapiFhirResourceDaoTest {
|
||||
|
||||
TestResourceDao mySvc = new TestResourceDao();
|
||||
|
||||
@Test
|
||||
|
|
|
@ -721,6 +721,4 @@ public abstract class BaseJpaTest extends BaseTest {
|
|||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.context.FhirVersionEnum;
|
|||
import ca.uhn.fhir.interceptor.executor.InterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4;
|
||||
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
|
||||
|
@ -70,6 +71,8 @@ public class TransactionProcessorTest {
|
|||
private MatchUrlService myMatchUrlService;
|
||||
@MockBean
|
||||
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
||||
@MockBean
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
|
||||
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private SessionImpl mySession;
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.dao.index;
|
||||
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class IdHelperServiceTest {
|
||||
|
||||
@Test
|
||||
public void testReplaceDefault_AllPartitions() {
|
||||
|
||||
IdHelperService svc = new IdHelperService();
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setDefaultPartitionId(1);
|
||||
svc.setPartitionSettingsForUnitTest(partitionSettings);
|
||||
|
||||
RequestPartitionId outcome = svc.replaceDefault(RequestPartitionId.allPartitions());
|
||||
assertSame(RequestPartitionId.allPartitions(), outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceDefault_DefaultPartition() {
|
||||
|
||||
IdHelperService svc = new IdHelperService();
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setDefaultPartitionId(1);
|
||||
svc.setPartitionSettingsForUnitTest(partitionSettings);
|
||||
|
||||
RequestPartitionId outcome = svc.replaceDefault(RequestPartitionId.defaultPartition());
|
||||
assertEquals(1, outcome.getPartitionIds().get(0));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
package ca.uhn.fhir.jpa.dao.index;
|
||||
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceVersionSvcDaoImpl;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Path;
|
||||
import javax.persistence.criteria.Root;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ResourceVersionSvcTest {
|
||||
|
||||
// helper class to package up data for helper methods
|
||||
private class ResourceIdPackage {
|
||||
public IIdType MyResourceId;
|
||||
public ResourcePersistentId MyPid;
|
||||
public Long MyVersion;
|
||||
|
||||
public ResourceIdPackage(IIdType id,
|
||||
ResourcePersistentId pid,
|
||||
Long version) {
|
||||
MyResourceId = id;
|
||||
MyPid = pid;
|
||||
MyVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
@Mock
|
||||
DaoRegistry myDaoRegistry;
|
||||
@Mock
|
||||
IResourceTableDao myResourceTableDao;
|
||||
@Mock
|
||||
IdHelperService myIdHelperService;
|
||||
|
||||
// TODO KHS move the methods that use this out to a separate test class
|
||||
@InjectMocks
|
||||
private ResourceVersionSvcDaoImpl myResourceVersionSvc;
|
||||
|
||||
/**
|
||||
* Gets a ResourceTable record for getResourceVersionsForPid
|
||||
* Order matters!
|
||||
* @param resourceType
|
||||
* @param pid
|
||||
* @param version
|
||||
* @return
|
||||
*/
|
||||
private Object[] getResourceTableRecordForResourceTypeAndPid(String resourceType, long pid, long version) {
|
||||
return new Object[] {
|
||||
pid, // long
|
||||
resourceType, // string
|
||||
version // long
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock out resolveResourcePersistentIdsWithCache
|
||||
* to return empty lists (as if no resources were found).
|
||||
*/
|
||||
private void mock_resolveResourcePersistentIdsWithCache_toReturnNothing() {
|
||||
CriteriaBuilder cb = Mockito.mock(CriteriaBuilder.class);
|
||||
CriteriaQuery<ForcedId> criteriaQuery = Mockito.mock(CriteriaQuery.class);
|
||||
Root<ForcedId> from = Mockito.mock(Root.class);
|
||||
Path path = Mockito.mock(Path.class);
|
||||
|
||||
TypedQuery<ForcedId> queryMock = Mockito.mock(TypedQuery.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock out getIdsOfExistingResources
|
||||
* to return the matches and resources matching those provided
|
||||
* by parameters.
|
||||
* @param theResourcePacks
|
||||
*/
|
||||
private void mockReturnsFor_getIdsOfExistingResources(ResourceIdPackage... theResourcePacks) {
|
||||
List<ResourcePersistentId> resourcePersistentIds = new ArrayList<>();
|
||||
List<Object[]> matches = new ArrayList<>();
|
||||
|
||||
for (ResourceIdPackage pack : theResourcePacks) {
|
||||
resourcePersistentIds.add(pack.MyPid);
|
||||
|
||||
matches.add(getResourceTableRecordForResourceTypeAndPid(
|
||||
pack.MyResourceId.getResourceType(),
|
||||
pack.MyPid.getIdAsLong(),
|
||||
pack.MyVersion
|
||||
));
|
||||
}
|
||||
|
||||
ResourcePersistentId first = resourcePersistentIds.remove(0);
|
||||
if (resourcePersistentIds.isEmpty()) {
|
||||
when(myIdHelperService.resolveResourcePersistentIdsWithCache(any(), any())).thenReturn(Collections.singletonList(first));
|
||||
}
|
||||
else {
|
||||
when(myIdHelperService.resolveResourcePersistentIdsWithCache(any(), any())).thenReturn(resourcePersistentIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLatestVersionIdsForResourceIds_whenResourceExists_returnsMapWithPIDAndVersion() {
|
||||
IIdType type = new IdDt("Patient/RED");
|
||||
ResourcePersistentId pid = new ResourcePersistentId(1L);
|
||||
pid.setAssociatedResourceId(type);
|
||||
HashMap<IIdType, ResourcePersistentId> map = new HashMap<>();
|
||||
map.put(type, pid);
|
||||
ResourceIdPackage pack = new ResourceIdPackage(type, pid, 2L);
|
||||
|
||||
// when
|
||||
mockReturnsFor_getIdsOfExistingResources(pack);
|
||||
|
||||
// test
|
||||
ResourcePersistentIdMap retMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
|
||||
Collections.singletonList(type));
|
||||
|
||||
Assertions.assertTrue(retMap.containsKey(type));
|
||||
Assertions.assertEquals(pid.getVersion(), map.get(type).getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLatestVersionIdsForResourceIds_whenResourceDoesNotExist_returnsEmptyMap() {
|
||||
IIdType type = new IdDt("Patient/RED");
|
||||
|
||||
// when
|
||||
mock_resolveResourcePersistentIdsWithCache_toReturnNothing();
|
||||
|
||||
// test
|
||||
ResourcePersistentIdMap retMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
|
||||
Collections.singletonList(type));
|
||||
|
||||
Assertions.assertTrue(retMap.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLatestVersionIdsForResourceIds_whenSomeResourcesDoNotExist_returnsOnlyExistingElements() {
|
||||
// resource to be found
|
||||
IIdType type = new IdDt("Patient/RED");
|
||||
ResourcePersistentId pid = new ResourcePersistentId(1L);
|
||||
pid.setAssociatedResourceId(type);
|
||||
ResourceIdPackage pack = new ResourceIdPackage(type, pid, 2L);
|
||||
|
||||
// resource that won't be found
|
||||
IIdType type2 = new IdDt("Patient/BLUE");
|
||||
|
||||
// when
|
||||
mock_resolveResourcePersistentIdsWithCache_toReturnNothing();
|
||||
mockReturnsFor_getIdsOfExistingResources(pack);
|
||||
|
||||
// test
|
||||
ResourcePersistentIdMap retMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(
|
||||
RequestPartitionId.allPartitions(),
|
||||
Arrays.asList(type, type2)
|
||||
);
|
||||
|
||||
// verify
|
||||
Assertions.assertEquals(1, retMap.size());
|
||||
Assertions.assertTrue(retMap.containsKey(type));
|
||||
Assertions.assertFalse(retMap.containsKey(type2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceDefault_AllPartitions() {
|
||||
|
||||
IdHelperService svc = new IdHelperService();
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setDefaultPartitionId(1);
|
||||
svc.setPartitionSettingsForUnitTest(partitionSettings);
|
||||
|
||||
RequestPartitionId outcome = svc.replaceDefault(RequestPartitionId.allPartitions());
|
||||
assertSame(RequestPartitionId.allPartitions(), outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceDefault_DefaultPartition() {
|
||||
|
||||
IdHelperService svc = new IdHelperService();
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setDefaultPartitionId(1);
|
||||
svc.setPartitionSettingsForUnitTest(partitionSettings);
|
||||
|
||||
RequestPartitionId outcome = svc.replaceDefault(RequestPartitionId.defaultPartition());
|
||||
assertEquals(1, outcome.getPartitionIds().get(0));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.ResourceSearch;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
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.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||
|
||||
@Autowired
|
||||
MatchUrlService myMatchUrlService;
|
||||
|
||||
@AfterEach
|
||||
public void after() throws Exception {
|
||||
|
||||
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
|
||||
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
|
||||
myDaoConfig.setCountSearchResultsUpTo(new DaoConfig().getCountSearchResultsUpTo());
|
||||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
|
||||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
|
||||
myModelConfig.setIndexOnContainedResources(false);
|
||||
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
|
||||
|
||||
myDaoConfig.setAllowMultipleDelete(true);
|
||||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
myModelConfig.setIndexOnContainedResources(true);
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveATwoLinkChainWithStandAloneResources() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.name=Smith";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveATwoLinkChainWithAContainedResource() throws Exception {
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Patient p = new Patient();
|
||||
p.setId("pat");
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getContained().add(p);
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.setValue(new StringType("Test"));
|
||||
obs.getSubject().setReference("#pat");
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.name=Smith";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
myOrganizationDao.create(org, mySrd);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference(org.getId());
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheEndOfTheChain() throws Exception {
|
||||
// This is the case that is most relevant to SMILE-2899
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId("org");
|
||||
org.setName("HealthCo");
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.getContained().add(org);
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference("#org");
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheBeginningOfTheChain() throws Exception {
|
||||
// We do not currently support this case - we may not be indexing the references of contained resources
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
myOrganizationDao.create(org, mySrd);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId("pat");
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference(org.getId());
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getContained().add(p);
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference("#pat");
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
myOrganizationDao.create(org, mySrd);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference(org.getId());
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWithQualifiersWithAContainedResourceAtTheEndOfTheChain() throws Exception {
|
||||
// This is the case that is most relevant to SMILE-2899
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId("org");
|
||||
org.setName("HealthCo");
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.getContained().add(org);
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference("#org");
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
myOrganizationDao.create(org, mySrd);
|
||||
|
||||
Organization partOfOrg = new Organization();
|
||||
partOfOrg.setId(IdType.newRandomUuid());
|
||||
partOfOrg.getPartOf().setReference(org.getId());
|
||||
myOrganizationDao.create(partOfOrg, mySrd);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference(partOfOrg.getId());
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.partof.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAFourLinkChainWhereTheLastReferenceIsContained() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId("parent");
|
||||
org.setName("HealthCo");
|
||||
|
||||
Organization partOfOrg = new Organization();
|
||||
partOfOrg.setId(IdType.newRandomUuid());
|
||||
partOfOrg.getContained().add(org);
|
||||
partOfOrg.getPartOf().setReference("#parent");
|
||||
myOrganizationDao.create(partOfOrg, mySrd);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getManagingOrganization().setReference(partOfOrg.getId());
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.partof.name=HealthCo";
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
|
||||
List<String> ids = new ArrayList<>();
|
||||
|
||||
ResourceSearch search = myMatchUrlService.getResourceSearch(theUrl);
|
||||
SearchParameterMap map = search.getSearchParameterMap();
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider result = myObservationDao.search(map);
|
||||
return result.getAllResourceIds();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.config.ElasticsearchWithPrefixConfig;
|
||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchRestClientFactory;
|
||||
import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.client.RestClient;
|
||||
import org.elasticsearch.client.RestHighLevelClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
@RequiresDocker
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {ElasticsearchWithPrefixConfig.class})
|
||||
public class ElasticsearchPrefixTest {
|
||||
|
||||
@Autowired
|
||||
ElasticsearchContainer elasticsearchContainer;
|
||||
|
||||
public static String ELASTIC_PREFIX = "hapi-fhir";
|
||||
@Test
|
||||
public void test() throws IOException {
|
||||
//Given
|
||||
RestHighLevelClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
|
||||
"http", elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
|
||||
//When
|
||||
RestClient lowLevelClient = elasticsearchHighLevelRestClient.getLowLevelClient();
|
||||
Response get = lowLevelClient.performRequest(new Request("GET", "/_cat/indices"));
|
||||
String catIndexes = EntityUtils.toString(get.getEntity());
|
||||
|
||||
//Then
|
||||
assertThat(catIndexes, containsString(ELASTIC_PREFIX + "-resourcetable-000001"));
|
||||
assertThat(catIndexes, containsString(ELASTIC_PREFIX + "-termconcept-000001"));
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.AuditEvent;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
|
@ -21,6 +26,7 @@ import org.hl7.fhir.r4.model.Patient;
|
|||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.hl7.fhir.r4.model.Task;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -48,12 +54,11 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
|
|||
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
|
||||
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new DaoConfig().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
|
||||
myDaoConfig.setBundleTypesAllowedForStorage(new DaoConfig().getBundleTypesAllowedForStorage());
|
||||
|
||||
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWithBadReferenceFails() {
|
||||
|
||||
Observation o = new Observation();
|
||||
o.setStatus(ObservationStatus.FINAL);
|
||||
o.getSubject().setReference("Patient/FOO");
|
||||
|
@ -97,27 +102,23 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
|
|||
params.add(Task.SP_PART_OF, new ReferenceParam("Task/AAA"));
|
||||
List<String> found = toUnqualifiedVersionlessIdValues(myTaskDao.search(params));
|
||||
assertThat(found, contains(id.getValue()));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithBadReferenceFails() {
|
||||
Observation o1 = new Observation();
|
||||
o1.setStatus(ObservationStatus.FINAL);
|
||||
IIdType id = myObservationDao.create(o1, mySrd).getId();
|
||||
|
||||
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 {
|
||||
|
||||
Exception ex = Assertions.assertThrows(InvalidRequestException.class, () -> {
|
||||
myObservationDao.update(o, mySrd);
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertThat(e.getMessage(), startsWith("Resource Patient/FOO not found, specified in path: Observation.subject"));
|
||||
}
|
||||
});
|
||||
assertThat(ex.getMessage(), startsWith("Resource Patient/FOO not found, specified in path: Observation.subject"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -450,8 +451,6 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
|
|||
ourLog.info("\nObservation read after Patient update:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
|
||||
assertEquals(createdObs.getSubject().getReference(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
|
||||
assertEquals(placeholderPatId.toUnqualifiedVersionless().getValueAsString(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -540,7 +539,6 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
|
|||
|
||||
AuditEvent createdEvent = myAuditEventDao.read(id);
|
||||
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEvent));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -560,11 +558,96 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
|
|||
IIdType id = myObservationDao.create(obsToCreate, mySrd).getId();
|
||||
|
||||
Observation createdObs = myObservationDao.read(id);
|
||||
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
|
||||
assertEquals("Patient/ABC", createdObs.getSubject().getReference());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutocreatePlaceholderTest() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
Reference patientRef = new Reference("Patient/RED");
|
||||
obs.setSubject(patientRef);
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
builder.addTransactionUpdateEntry(obs);
|
||||
|
||||
mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
|
||||
|
||||
// verify subresource is created
|
||||
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
|
||||
assertNotNull(returned);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAutocreatePlaceholderWithTargetExistingAlreadyTest() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
String patientId = "Patient/RED";
|
||||
|
||||
// create
|
||||
Patient patient = new Patient();
|
||||
patient.setIdElement(new IdType(patientId));
|
||||
myPatientDao.update(patient); // use update to use forcedid
|
||||
|
||||
// update
|
||||
patient.setActive(true);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
// observation (with version 2)
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
Reference patientRef = new Reference(patientId);
|
||||
obs.setSubject(patientRef);
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
builder.addTransactionUpdateEntry(obs);
|
||||
|
||||
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
|
||||
|
||||
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
|
||||
assertNotNull(returned);
|
||||
Assertions.assertTrue(returned.getActive());
|
||||
Assertions.assertEquals(2, returned.getIdElement().getVersionIdPartAsLong());
|
||||
|
||||
Observation retObservation = myObservationDao.read(obs.getIdElement());
|
||||
assertNotNull(retObservation);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test is the same as above, except it uses the serverid (instead of forcedid)
|
||||
*/
|
||||
@Test
|
||||
public void testAutocreatePlaceholderWithExistingTargetWithServerAssignedIdTest() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
// create
|
||||
Patient patient = new Patient();
|
||||
patient.setIdElement(new IdType("Patient"));
|
||||
DaoMethodOutcome ret = myPatientDao.create(patient); // use create to use server id
|
||||
|
||||
// update - to update our version
|
||||
patient.setActive(true);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
// observation (with version 2)
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
Reference patientRef = new Reference("Patient/" + ret.getId().getIdPart());
|
||||
obs.setSubject(patientRef);
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
builder.addTransactionUpdateEntry(obs);
|
||||
|
||||
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
|
||||
|
||||
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
|
||||
assertNotNull(returned);
|
||||
Assertions.assertEquals(2, returned.getIdElement().getVersionIdPartAsLong());
|
||||
|
||||
Observation retObservation = myObservationDao.read(obs.getIdElement());
|
||||
assertNotNull(retObservation);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Address;
|
||||
import org.hl7.fhir.r4.model.Address.AddressUse;
|
||||
|
@ -26,15 +26,13 @@ import org.junit.jupiter.api.AfterEach;
|
|||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ContainedTest.class);
|
||||
|
||||
|
@ -80,7 +78,6 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test {
|
|||
map = new SearchParameterMap();
|
||||
map.add("subject", new ReferenceParam("name", "Smith"));
|
||||
map.setSearchContainedMode(SearchContainedModeEnum.TRUE);
|
||||
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id)));
|
||||
}
|
||||
|
||||
|
@ -110,14 +107,16 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test {
|
|||
.getSingleResult();
|
||||
assertEquals(1L, i.longValue());
|
||||
});
|
||||
|
||||
|
||||
SearchParameterMap map;
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add("subject", new ReferenceParam("name", "Smith"));
|
||||
map.setSearchContainedMode(SearchContainedModeEnum.TRUE);
|
||||
|
||||
map.setLoadSynchronous(true);
|
||||
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,11 +9,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
|
||||
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
|
||||
import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
|
||||
import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.CodeSystem;
|
||||
|
@ -28,6 +32,8 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
@ -341,6 +347,4 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
|
@ -20,8 +22,11 @@ import org.hl7.fhir.r4.model.Patient;
|
|||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.hl7.fhir.r4.model.Task;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
|
@ -30,9 +35,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||
|
@ -90,7 +98,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
assertEquals("Patient/A/_history/1", eob2.getPatient().getReference());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCreateAndUpdateVersionedReferencesInTransaction_VersionedReferenceToVersionedReferenceToUpsertWithNop() {
|
||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
|
@ -283,7 +290,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
myObservationDao.update(observation);
|
||||
|
||||
// Make sure we're not introducing any extra DB operations
|
||||
assertEquals(5, myCaptureQueriesListener.logSelectQueries().size());
|
||||
assertEquals(6, myCaptureQueriesListener.logSelectQueries().size());
|
||||
|
||||
// Read back and verify that reference is now versioned
|
||||
observation = myObservationDao.read(observationId);
|
||||
|
@ -295,7 +302,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
|
||||
Patient patient = new Patient();
|
||||
|
@ -326,7 +332,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
observation = myObservationDao.read(observationId);
|
||||
assertEquals(patientId.getValue(), observation.getSubject().getReference());
|
||||
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -389,7 +394,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
assertEquals(patientId.getValue(), observation.getSubject().getReference());
|
||||
assertEquals("2", observation.getSubject().getReferenceElement().getVersionIdPart());
|
||||
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -409,7 +413,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
// Update patient to make a second version
|
||||
patient.setActive(false);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
}
|
||||
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
|
@ -458,7 +461,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
// Update patient to make a second version
|
||||
patient.setActive(false);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
}
|
||||
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
|
@ -491,10 +493,8 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
// Read back and verify that reference is now versioned
|
||||
observation = myObservationDao.read(observationId);
|
||||
assertEquals(patientId.getValue(), observation.getSubject().getReference());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchAndIncludeVersionedReference_Asynchronous() {
|
||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
|
@ -780,4 +780,120 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoNpeOnEoBBundle() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
List<String> strings = Arrays.asList(
|
||||
"ExplanationOfBenefit.patient",
|
||||
"ExplanationOfBenefit.insurer",
|
||||
"ExplanationOfBenefit.provider",
|
||||
"ExplanationOfBenefit.careTeam.provider",
|
||||
"ExplanationOfBenefit.insurance.coverage",
|
||||
"ExplanationOfBenefit.payee.party"
|
||||
);
|
||||
myModelConfig.setAutoVersionReferenceAtPaths(new HashSet<>(strings));
|
||||
|
||||
Bundle bundle = myFhirCtx.newJsonParser().parseResource(Bundle.class,
|
||||
new InputStreamReader(
|
||||
FhirResourceDaoR4VersionedReferenceTest.class.getResourceAsStream("/npe-causing-bundle.json")));
|
||||
|
||||
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), bundle);
|
||||
|
||||
assertNotNull(transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoVersionPathsWithAutoCreatePlaceholders() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/CDE");
|
||||
obs.setSubject(new Reference("Patient/ABC"));
|
||||
DaoMethodOutcome update = myObservationDao.create(obs);
|
||||
Observation resource = (Observation)update.getResource();
|
||||
String versionedPatientReference = resource.getSubject().getReference();
|
||||
assertThat(versionedPatientReference, is(equalTo("Patient/ABC")));
|
||||
|
||||
Patient p = myPatientDao.read(new IdDt("Patient/ABC"));
|
||||
Assertions.assertNotNull(p);
|
||||
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
obs.setSubject(new Reference("Patient/RED"));
|
||||
update = myObservationDao.create(obs);
|
||||
resource = (Observation)update.getResource();
|
||||
versionedPatientReference = resource.getSubject().getReference();
|
||||
|
||||
assertThat(versionedPatientReference, is(equalTo("Patient/RED/_history/1")));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("Bundle transaction with AutoVersionReferenceAtPath on and with existing Patient resource should create")
|
||||
public void bundleTransaction_autoreferenceAtPathWithPreexistingPatientReference_shouldCreate() {
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
String patientId = "Patient/RED";
|
||||
IIdType idType = new IdDt(patientId);
|
||||
|
||||
// create patient ahead of time
|
||||
Patient patient = new Patient();
|
||||
patient.setId(patientId);
|
||||
DaoMethodOutcome outcome = myPatientDao.update(patient);
|
||||
assertThat(outcome.getResource().getIdElement().getValue(), is(equalTo(patientId + "/_history/1")));
|
||||
|
||||
Patient returned = myPatientDao.read(idType);
|
||||
Assertions.assertNotNull(returned);
|
||||
assertThat(returned.getId(), is(equalTo(patientId + "/_history/1")));
|
||||
|
||||
// update to change version
|
||||
patient.setActive(true);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
Reference patientRef = new Reference(patientId);
|
||||
obs.setSubject(patientRef);
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
builder.addTransactionUpdateEntry(obs);
|
||||
|
||||
Bundle submitted = (Bundle)builder.getBundle();
|
||||
|
||||
Bundle returnedTr = mySystemDao.transaction(new SystemRequestDetails(), submitted);
|
||||
|
||||
Assertions.assertNotNull(returnedTr);
|
||||
|
||||
// some verification
|
||||
Observation obRet = myObservationDao.read(obs.getIdElement());
|
||||
Assertions.assertNotNull(obRet);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GH-2901 Test no NPE is thrown on autoversioned references")
|
||||
public void testNoNpeMinimal() {
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.setId("Observation/DEF");
|
||||
Reference patientRef = new Reference("Patient/RED");
|
||||
obs.setSubject(patientRef);
|
||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||
builder.addTransactionUpdateEntry(obs);
|
||||
|
||||
Bundle submitted = (Bundle)builder.getBundle();
|
||||
|
||||
Bundle returnedTr = mySystemDao.transaction(new SystemRequestDetails(), submitted);
|
||||
|
||||
Assertions.assertNotNull(returnedTr);
|
||||
|
||||
// some verification
|
||||
Observation obRet = myObservationDao.read(obs.getIdElement());
|
||||
Assertions.assertNotNull(obRet);
|
||||
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
|
||||
Assertions.assertNotNull(returned);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
|
||||
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
|
||||
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
|
@ -119,6 +120,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
||||
myDaoConfig.setBundleBatchPoolSize(new DaoConfig().getBundleBatchPoolSize());
|
||||
myDaoConfig.setBundleBatchMaxPoolSize(new DaoConfig().getBundleBatchMaxPoolSize());
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets());
|
||||
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
|
|
@ -249,6 +249,45 @@ public class ResourceProviderR4BundleTest extends BaseResourceProviderR4Test {
|
|||
assertEquals(ids.get(4), bundleEntries.get(6).getResource().getIdElement().toUnqualifiedVersionless().getValueAsString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTagCacheWorksWithBatchMode() {
|
||||
Bundle input = new Bundle();
|
||||
input.setType(BundleType.BATCH);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId("100");
|
||||
p.setGender(AdministrativeGender.MALE);
|
||||
p.addIdentifier().setSystem("urn:foo").setValue("A");
|
||||
p.addName().setFamily("Smith");
|
||||
p.getMeta().addTag().setSystem("mysystem").setCode("mycode");
|
||||
input.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST);
|
||||
|
||||
Patient p2 = new Patient();
|
||||
p2.setId("200");
|
||||
p2.setGender(AdministrativeGender.MALE);
|
||||
p2.addIdentifier().setSystem("urn:foo").setValue("A");
|
||||
p2.addName().setFamily("Smith");
|
||||
p2.getMeta().addTag().setSystem("mysystem").setCode("mycode");
|
||||
input.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST);
|
||||
|
||||
Patient p3 = new Patient();
|
||||
p3.setId("pat-300");
|
||||
p3.setGender(AdministrativeGender.MALE);
|
||||
p3.addIdentifier().setSystem("urn:foo").setValue("A");
|
||||
p3.addName().setFamily("Smith");
|
||||
p3.getMeta().addTag().setSystem("mysystem").setCode("mycode");
|
||||
input.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.PUT).setUrl("Patient/pat-300");
|
||||
|
||||
Bundle output = myClient.transaction().withBundle(input).execute();
|
||||
output.getEntry().stream()
|
||||
.map(BundleEntryComponent::getResponse)
|
||||
.map(Bundle.BundleEntryResponseComponent::getStatus)
|
||||
.forEach(statusCode -> {
|
||||
assertEquals(statusCode, "201 Created");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private List<String> createPatients(int count) {
|
||||
List<String> ids = new ArrayList<String>();
|
||||
|
|
|
@ -23,17 +23,11 @@ class ElasticsearchHibernatePropertiesBuilderTest {
|
|||
|
||||
ElasticsearchHibernatePropertiesBuilder myPropertiesBuilder = spy(ElasticsearchHibernatePropertiesBuilder.class);
|
||||
|
||||
@BeforeEach
|
||||
public void prepMocks() {
|
||||
//ensures we don't try to reach out to a real ES server on apply.
|
||||
doNothing().when(myPropertiesBuilder).injectStartupTemplate(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestUrlCannotContainProtocol() {
|
||||
public void testHostsCannotContainProtocol() {
|
||||
String host = "localhost:9200";
|
||||
String protocolHost = "https://" + host;
|
||||
String failureMessage = "Elasticsearch URL cannot include a protocol, that is a separate property. Remove http:// or https:// from this URL.";
|
||||
String failureMessage = "Elasticsearch URLs cannot include a protocol, that is a separate property. Remove http:// or https:// from this URL.";
|
||||
|
||||
myPropertiesBuilder
|
||||
.setProtocol("https")
|
||||
|
@ -42,19 +36,42 @@ class ElasticsearchHibernatePropertiesBuilderTest {
|
|||
|
||||
//SUT
|
||||
try {
|
||||
myPropertiesBuilder.setRestUrl(protocolHost);
|
||||
myPropertiesBuilder.setHosts(protocolHost)
|
||||
.apply(new Properties());
|
||||
fail();
|
||||
} catch (ConfigurationException e ) {
|
||||
assertThat(e.getMessage(), is(equalTo(failureMessage)));
|
||||
}
|
||||
|
||||
doNothing().when(myPropertiesBuilder).injectStartupTemplate(any(), any(), any(), any());
|
||||
Properties properties = new Properties();
|
||||
myPropertiesBuilder
|
||||
.setRestUrl(host)
|
||||
.setHosts(host)
|
||||
.apply(properties);
|
||||
|
||||
assertThat(properties.getProperty(BackendSettings.backendKey(ElasticsearchBackendSettings.HOSTS)), is(equalTo(host)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostsValueValidation() {
|
||||
String host = "localhost_9200,localhost:9201,localhost:9202";
|
||||
String failureMessage = "Elasticsearch URLs have to contain ':' as a host:port separator. Example: localhost:9200,localhost:9201,localhost:9202";
|
||||
|
||||
myPropertiesBuilder
|
||||
.setProtocol("https")
|
||||
.setHosts(host)
|
||||
.setUsername("whatever")
|
||||
.setPassword("whatever");
|
||||
|
||||
//SUT
|
||||
try {
|
||||
myPropertiesBuilder
|
||||
.apply(new Properties());
|
||||
fail();
|
||||
} catch (ConfigurationException e ) {
|
||||
assertThat(e.getMessage(), is(equalTo(failureMessage)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
|
|||
public void before() throws IOException {
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setPartitioningEnabled(false);
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost(), elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), null, null);
|
||||
|
||||
if (!indexLoaded) {
|
||||
createMultiplePatientsAndObservations();
|
||||
|
|
|
@ -97,7 +97,7 @@ public class LastNElasticsearchSvcSingleObservationIT {
|
|||
public void before() {
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setPartitioningEnabled(false);
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost(), elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
|
|
@ -14,6 +14,7 @@ import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCache;
|
|||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
|
||||
import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
|
||||
import ca.uhn.fhir.jpa.dao.JpaResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
|
||||
|
@ -45,6 +46,7 @@ import com.google.common.collect.Lists;
|
|||
import org.hamcrest.Matchers;
|
||||
import org.hibernate.internal.SessionImpl;
|
||||
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.ExplanationOfBenefit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
@ -323,10 +325,15 @@ public class GiantTransactionPerfTest {
|
|||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
public ResourceVersionMap getVersionMap(RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
myGetVersionMap++;
|
||||
return ResourceVersionMap.fromResources(Lists.newArrayList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(RequestPartitionId thePartition, List<IIdType> theIds) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class MockResourceHistoryTableDao implements IResourceHistoryTableDao {
|
||||
|
|
|
@ -0,0 +1,402 @@
|
|||
{
|
||||
"resourceType": "Bundle",
|
||||
"type": "transaction",
|
||||
"entry": [ {
|
||||
"resource": {
|
||||
"resourceType": "ExplanationOfBenefit",
|
||||
"id": "26d4cebd-95c6-39ea-855c-dc819bc68d08",
|
||||
"meta": {
|
||||
"lastUpdated": "2016-01-01T00:56:00.000-05:00",
|
||||
"profile": [ "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Inpatient-Institutional" ]
|
||||
},
|
||||
"identifier": [ {
|
||||
"type": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
|
||||
"code": "uc"
|
||||
} ]
|
||||
},
|
||||
"system": "fhir/CodeSystem/sid/eob-inpatient-claim-id",
|
||||
"value": "20550047"
|
||||
} ],
|
||||
"status": "active",
|
||||
"type": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claim-type",
|
||||
"code": "institutional"
|
||||
} ]
|
||||
},
|
||||
"use": "claim",
|
||||
"patient": {
|
||||
"reference": "Patient/ABC"
|
||||
},
|
||||
"billablePeriod": {
|
||||
"start": "2015-12-14T00:00:00-05:00"
|
||||
},
|
||||
"created": "2021-08-16T13:54:10-04:00",
|
||||
"insurer": {
|
||||
"reference": "Organization/A"
|
||||
},
|
||||
"provider": {
|
||||
"reference": "Organization/b9d22776-1ee9-3843-bc48-b4bf67861483"
|
||||
},
|
||||
"outcome": "complete",
|
||||
"careTeam": [ {
|
||||
"sequence": 1,
|
||||
"provider": {
|
||||
"reference": "Practitioner/H"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claimcareteamrole",
|
||||
"code": "primary"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"sequence": 2,
|
||||
"provider": {
|
||||
"reference": "Practitioner/I"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "attending"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"sequence": 3,
|
||||
"provider": {
|
||||
"reference": "Practitioner/J"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "performing"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"supportingInfo": [ {
|
||||
"sequence": 1,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "admissionperiod"
|
||||
} ]
|
||||
},
|
||||
"timingPeriod": {
|
||||
"start": "2015-12-14T00:00:00-05:00",
|
||||
"end": "2016-01-11T14:11:00-05:00"
|
||||
}
|
||||
}, {
|
||||
"sequence": 2,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "clmrecvddate"
|
||||
} ]
|
||||
},
|
||||
"timingDate": "2018-12-23"
|
||||
}, {
|
||||
"sequence": 3,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "admtype"
|
||||
} ]
|
||||
},
|
||||
"code": {
|
||||
"coding": [ {
|
||||
"system": "https://www.nubc.org/CodeSystem/PriorityTypeOfAdmitOrVisit",
|
||||
"code": "4"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"diagnosis": [ {
|
||||
"sequence": 1,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "Z38.00"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
} ],
|
||||
"insurance": [ {
|
||||
"focal": true,
|
||||
"coverage": {
|
||||
"reference": "Coverage/G"
|
||||
}
|
||||
} ],
|
||||
"total": [ {
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "benefit"
|
||||
} ]
|
||||
},
|
||||
"amount": {
|
||||
"value": 1039.28
|
||||
}
|
||||
}, {
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "submitted"
|
||||
} ]
|
||||
},
|
||||
"amount": {
|
||||
"value": 2011
|
||||
}
|
||||
} ]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "ExplanationOfBenefit/26d4cebd-95c6-39ea-855c-dc819bc68d08"
|
||||
}
|
||||
}, {
|
||||
"resource": {
|
||||
"resourceType": "ExplanationOfBenefit",
|
||||
"id": "a25f1c3a-09b9-3f17-8f1b-0fbbf6391fce",
|
||||
"meta": {
|
||||
"lastUpdated": "2016-01-01T00:58:00.000-05:00",
|
||||
"profile": [ "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Inpatient-Institutional" ]
|
||||
},
|
||||
"identifier": [ {
|
||||
"type": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
|
||||
"code": "uc"
|
||||
} ]
|
||||
},
|
||||
"system": "fhir/CodeSystem/sid/eob-inpatient-claim-id",
|
||||
"value": "20586901"
|
||||
} ],
|
||||
"status": "active",
|
||||
"type": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claim-type",
|
||||
"code": "institutional"
|
||||
} ]
|
||||
},
|
||||
"use": "claim",
|
||||
"patient": {
|
||||
"reference": "Patient/ABC"
|
||||
},
|
||||
"billablePeriod": {
|
||||
"start": "2015-12-18T00:00:00-05:00"
|
||||
},
|
||||
"created": "2021-08-16T13:54:10-04:00",
|
||||
"insurer": {
|
||||
"reference": "Organization/A"
|
||||
},
|
||||
"provider": {
|
||||
"reference": "Organization/d10823cf-ee15-3a0e-a12e-1509cd18cda4"
|
||||
},
|
||||
"outcome": "complete",
|
||||
"careTeam": [ {
|
||||
"sequence": 1,
|
||||
"provider": {
|
||||
"reference": "Practitioner/D"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claimcareteamrole",
|
||||
"code": "primary"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"sequence": 2,
|
||||
"provider": {
|
||||
"reference": "Practitioner/E"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "attending"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"sequence": 3,
|
||||
"provider": {
|
||||
"reference": "Practitioner/F"
|
||||
},
|
||||
"role": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "performing"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"supportingInfo": [ {
|
||||
"sequence": 1,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "admissionperiod"
|
||||
} ]
|
||||
},
|
||||
"timingPeriod": {
|
||||
"start": "2015-12-18T00:00:00-05:00",
|
||||
"end": "2016-01-11T14:12:00-05:00"
|
||||
}
|
||||
}, {
|
||||
"sequence": 2,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "clmrecvddate"
|
||||
} ]
|
||||
},
|
||||
"timingDate": "2018-12-29"
|
||||
}, {
|
||||
"sequence": 3,
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "admtype"
|
||||
} ]
|
||||
},
|
||||
"code": {
|
||||
"coding": [ {
|
||||
"system": "https://www.nubc.org/CodeSystem/PriorityTypeOfAdmitOrVisit",
|
||||
"code": "4"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"diagnosis": [ {
|
||||
"sequence": 1,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "Z38.00"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 2,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "P96.89"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 3,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "R25.8"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 4,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "P08.21"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 5,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "P92.5"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 6,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "P92.6"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"sequence": 7,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [ {
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "Z23"
|
||||
} ]
|
||||
},
|
||||
"type": [ {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
|
||||
"code": "principal"
|
||||
} ]
|
||||
} ]
|
||||
} ],
|
||||
"insurance": [ {
|
||||
"focal": true,
|
||||
"coverage": {
|
||||
"reference": "Coverage/G"
|
||||
}
|
||||
} ],
|
||||
"total": [ {
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "benefit"
|
||||
} ]
|
||||
},
|
||||
"amount": {
|
||||
"value": 1421.31
|
||||
}
|
||||
}, {
|
||||
"category": {
|
||||
"coding": [ {
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "submitted"
|
||||
} ]
|
||||
},
|
||||
"amount": {
|
||||
"value": 2336
|
||||
}
|
||||
} ]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "ExplanationOfBenefit/a25f1c3a-09b9-3f17-8f1b-0fbbf6391fce"
|
||||
}
|
||||
} ] }
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -20,9 +20,12 @@ package ca.uhn.fhir.jpa.cache;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This interface is used by the {@link IResourceChangeListenerCacheRefresher} to read resources matching the provided
|
||||
|
@ -30,5 +33,12 @@ import javax.annotation.Nonnull;
|
|||
*/
|
||||
public interface IResourceVersionSvc {
|
||||
@Nonnull
|
||||
ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap);
|
||||
ResourceVersionMap getVersionMap(RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap);
|
||||
|
||||
@Nonnull
|
||||
default ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
return getVersionMap(RequestPartitionId.allPartitions(), theResourceName, theSearchParamMap);
|
||||
}
|
||||
|
||||
ResourcePersistentIdMap getLatestVersionIdsForResourceIds(RequestPartitionId thePartition, List<IIdType> theIds);
|
||||
}
|
||||
|
|
|
@ -173,8 +173,8 @@ public class ResourceChangeListenerCacheRefresherImpl implements IResourceChange
|
|||
List<IIdType> updatedIds = new ArrayList<>();
|
||||
|
||||
for (IIdType id : theNewResourceVersionMap.keySet()) {
|
||||
String previousValue = theOldResourceVersionCache.put(id, theNewResourceVersionMap.get(id));
|
||||
IIdType newId = id.withVersion(theNewResourceVersionMap.get(id));
|
||||
Long previousValue = theOldResourceVersionCache.put(id, theNewResourceVersionMap.get(id));
|
||||
IIdType newId = id.withVersion(theNewResourceVersionMap.get(id).toString());
|
||||
if (previousValue == null) {
|
||||
createdIds.add(newId);
|
||||
} else if (!theNewResourceVersionMap.get(id).equals(previousValue)) {
|
||||
|
|
67
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourcePersistentIdMap.java
vendored
Normal file
67
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourcePersistentIdMap.java
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Search Parameters
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||
* %%
|
||||
* 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.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourcePersistentIdMap {
|
||||
private final Map<IIdType, ResourcePersistentId> myMap = new HashMap<>();
|
||||
|
||||
public static ResourcePersistentIdMap fromResourcePersistentIds(List<ResourcePersistentId> theResourcePersistentIds) {
|
||||
ResourcePersistentIdMap retval = new ResourcePersistentIdMap();
|
||||
theResourcePersistentIds.forEach(retval::add);
|
||||
return retval;
|
||||
}
|
||||
|
||||
private void add(ResourcePersistentId theResourcePersistentId) {
|
||||
IIdType id = theResourcePersistentId.getAssociatedResourceId();
|
||||
myMap.put(id.toUnqualifiedVersionless(), theResourcePersistentId);
|
||||
}
|
||||
|
||||
public boolean containsKey(IIdType theId) {
|
||||
return myMap.containsKey(theId.toUnqualifiedVersionless());
|
||||
}
|
||||
|
||||
public ResourcePersistentId getResourcePersistentId(IIdType theId) {
|
||||
return myMap.get(theId.toUnqualifiedVersionless());
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return myMap.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return myMap.size();
|
||||
}
|
||||
|
||||
public void put(IIdType theId, ResourcePersistentId thePid) {
|
||||
myMap.put(theId, thePid);
|
||||
}
|
||||
|
||||
public void putAll(ResourcePersistentIdMap theIdAndPID) {
|
||||
myMap.putAll(theIdAndPID.myMap);
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import java.util.Set;
|
|||
* detect resources that were modified on remote servers in our cluster.
|
||||
*/
|
||||
public class ResourceVersionCache {
|
||||
private final Map<IIdType, String> myVersionMap = new HashMap<>();
|
||||
private final Map<IIdType, Long> myVersionMap = new HashMap<>();
|
||||
|
||||
public void clear() {
|
||||
myVersionMap.clear();
|
||||
|
@ -43,15 +43,15 @@ public class ResourceVersionCache {
|
|||
* @param theVersion
|
||||
* @return previous value
|
||||
*/
|
||||
public String put(IIdType theResourceId, String theVersion) {
|
||||
public Long put(IIdType theResourceId, Long theVersion) {
|
||||
return myVersionMap.put(new IdDt(theResourceId).toVersionless(), theVersion);
|
||||
}
|
||||
|
||||
public String getVersionForResourceId(IIdType theResourceId) {
|
||||
public Long getVersionForResourceId(IIdType theResourceId) {
|
||||
return myVersionMap.get(new IdDt(theResourceId));
|
||||
}
|
||||
|
||||
public String removeResourceId(IIdType theResourceId) {
|
||||
public Long removeResourceId(IIdType theResourceId) {
|
||||
return myVersionMap.remove(new IdDt(theResourceId));
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
|||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -36,8 +38,10 @@ import java.util.Set;
|
|||
* This immutable map holds a copy of current resource versions read from the repository.
|
||||
*/
|
||||
public class ResourceVersionMap {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ResourceVersionMap.class);
|
||||
private final Set<IIdType> mySourceIds = new HashSet<>();
|
||||
private final Map<IIdType, String> myMap = new HashMap<>();
|
||||
// Key versionless id, value version
|
||||
private final Map<IIdType, Long> myMap = new HashMap<>();
|
||||
private ResourceVersionMap() {}
|
||||
|
||||
public static ResourceVersionMap fromResourceTableEntities(List<ResourceTable> theEntities) {
|
||||
|
@ -57,13 +61,17 @@ public class ResourceVersionMap {
|
|||
}
|
||||
|
||||
private void add(IIdType theId) {
|
||||
if (theId.getVersionIdPart() == null) {
|
||||
ourLog.warn("Not storing {} in ResourceVersionMap because it does not have a version.", theId);
|
||||
return;
|
||||
}
|
||||
IdDt id = new IdDt(theId);
|
||||
mySourceIds.add(id);
|
||||
myMap.put(id.toUnqualifiedVersionless(), id.getVersionIdPart());
|
||||
myMap.put(id.toUnqualifiedVersionless(), id.getVersionIdPartAsLong());
|
||||
}
|
||||
|
||||
public String getVersion(IIdType theResourceId) {
|
||||
return myMap.get(new IdDt(theResourceId.toUnqualifiedVersionless()));
|
||||
public Long getVersion(IIdType theResourceId) {
|
||||
return get(theResourceId);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
|
@ -78,11 +86,15 @@ public class ResourceVersionMap {
|
|||
return Collections.unmodifiableSet(mySourceIds);
|
||||
}
|
||||
|
||||
public String get(IIdType theId) {
|
||||
public Long get(IIdType theId) {
|
||||
return myMap.get(new IdDt(theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
|
||||
public boolean containsKey(IIdType theId) {
|
||||
return myMap.containsKey(new IdDt(theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return myMap.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
@ -94,8 +95,8 @@ public class TestJpaDstu3Config extends BaseJavaConfigDstu3 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
@ -94,8 +95,8 @@ public class TestJpaR4Config extends BaseJavaConfigR4 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaR4");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>5.6.0-PRE3-SNAPSHOT</version>
|
||||
<version>5.6.0-PRE4-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
|
|||
import org.hl7.fhir.dstu2.model.Subscription;
|
||||
import org.hl7.fhir.r5.utils.IResourceValidator;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -134,8 +135,8 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
|
|||
|
||||
@Override
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(ConfigurableListableBeanFactory theConfigurableListableBeanFactory) {
|
||||
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(theConfigurableListableBeanFactory);
|
||||
retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu2");
|
||||
retVal.setDataSource(dataSource());
|
||||
retVal.setJpaProperties(jpaProperties());
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue