Partition aware search cleanup (#4706)

* Partition aware search cleanup

* Compile fixes

* Build fixes

* HAPI FHIR version bump

* License

* License header

* Tests
This commit is contained in:
James Agnew 2023-04-02 11:50:20 -04:00 committed by GitHub
parent 4fbeeccda4
commit a2bc9a7212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 454 additions and 190 deletions

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,14 +4,16 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -11,7 +11,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -120,8 +120,7 @@ public class SearchConfig {
mySearchBuilderFactory,
mySynchronousSearchSvc,
myPersistedJpaBundleProviderFactory,
myRequestPartitionHelperService,
mySearchParamRegistry,
mySearchParamRegistry,
mySearchStrategyFactory,
exceptionService(),
myBeanFactory

View File

@ -249,7 +249,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
Optional<Search> searchOpt = myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.execute(() -> mySearchCacheSvc.fetchByUuid(myUuid));
.execute(() -> mySearchCacheSvc.fetchByUuid(myUuid, myRequestPartitionId));
if (!searchOpt.isPresent()) {
return false;
}
@ -404,7 +404,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) {
return null;
} else {
return mySearchCoordinatorSvc.getSearchTotal(myUuid, myRequest).orElse(null);
return mySearchCoordinatorSvc.getSearchTotal(myUuid, myRequest, myRequestPartitionId).orElse(null);
}
}

View File

@ -39,7 +39,6 @@ import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.builder.StorageInterceptorHooksFacade;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchContinuationTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
@ -113,7 +112,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
private final BeanFactory myBeanFactory;
private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<>();
private final Consumer<String> myOnRemoveSearchTask = (theId) -> myIdToSearchTask.remove(theId);
private final Consumer<String> myOnRemoveSearchTask = myIdToSearchTask::remove;
private final StorageInterceptorHooksFacade myStorageInterceptorHooks;
private Integer myLoadingThrottleForUnitTests = null;
@ -135,7 +134,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
SearchBuilderFactory<JpaPid> theSearchBuilderFactory,
ISynchronousSearchSvc theSynchronousSearchSvc,
PersistedJpaBundleProviderFactory thePersistedJpaBundleProviderFactory,
IRequestPartitionHelperSvc theRequestPartitionHelperService,
ISearchParamRegistry theSearchParamRegistry,
SearchStrategyFactory theSearchStrategyFactory,
ExceptionService theExceptionSvc,
@ -245,7 +243,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
}
Callable<Search> searchCallback = () -> mySearchCacheSvc
.fetchByUuid(theUuid)
.fetchByUuid(theUuid, theRequestPartitionId)
.orElseThrow(() -> myExceptionSvc.newUnknownSearchException(theUuid));
search = myTxService
.withRequest(theRequestDetails)
@ -271,7 +269,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
// start a new pass
if (search.getStatus() == SearchStatusEnum.PASSCMPLET) {
ourLog.trace("Going to try to start next search");
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search);
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search, theRequestPartitionId);
if (newSearch.isPresent()) {
ourLog.trace("Launching new search");
search = newSearch.get();
@ -440,7 +438,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
}
@Override
public Optional<Integer> getSearchTotal(String theUuid, @Nullable RequestDetails theRequestDetails) {
public Optional<Integer> getSearchTotal(String theUuid, @Nullable RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
SearchTask task = myIdToSearchTask.get(theUuid);
if (task != null) {
return Optional.ofNullable(task.awaitInitialSync());
@ -450,7 +448,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
* In case there is no running search, if the total is listed as accurate we know one is coming
* so let's wait a bit for it to show up
*/
Optional<Search> search = myTxService.withRequest(theRequestDetails).execute(() -> mySearchCacheSvc.fetchByUuid(theUuid));
Optional<Search> search = myTxService.withRequest(theRequestDetails).execute(() -> mySearchCacheSvc.fetchByUuid(theUuid, theRequestPartitionId));
if (search.isPresent()) {
Optional<SearchParameterMap> searchParameterMap = search.get().getSearchParameterMap();
if (searchParameterMap.isPresent() && searchParameterMap.get().getSearchTotalMode() == SearchTotalModeEnum.ACCURATE) {
@ -461,7 +459,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
return Optional.of(search.get().getTotalCount());
}
}
search = mySearchCacheSvc.fetchByUuid(theUuid);
search = mySearchCacheSvc.fetchByUuid(theUuid, theRequestPartitionId);
}
}
}

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;
@ -50,7 +51,7 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc, IHas
@Override
@Transactional(propagation = Propagation.NEVER)
public void pollForStaleSearchesAndDeleteThem() {
mySearchCacheSvc.pollForStaleSearchesAndDeleteThem();
mySearchCacheSvc.pollForStaleSearchesAndDeleteThem(RequestPartitionId.allPartitions());
}
@Override

View File

@ -474,7 +474,7 @@ public class SearchTask implements Callable<Void> {
}
private void doSaveSearch() {
Search newSearch = mySearchCacheSvc.save(mySearch);
Search newSearch = mySearchCacheSvc.save(mySearch, myRequestPartitionId);
// mySearchDao.save is not supposed to return null, but in unit tests
// it can if the mock search dao isn't set up to handle that

View File

@ -25,6 +25,8 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.system.HapiSystemProperties;
@ -38,11 +40,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Instant;
import java.util.Collection;
@ -58,7 +57,7 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
*/
public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT = 500;
public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS = 20000;
public static final long SEARCH_CLEANUP_JOB_INTERVAL_MILLIS = 10 * DateUtils.MILLIS_PER_SECOND;
public static final long SEARCH_CLEANUP_JOB_INTERVAL_MILLIS = DateUtils.MILLIS_PER_MINUTE;
public static final int DEFAULT_MAX_DELETE_CANDIDATES_TO_FIND = 2000;
private static final Logger ourLog = LoggerFactory.getLogger(DatabaseSearchCacheSvcImpl.class);
private static int ourMaximumResultsToDeleteInOneStatement = DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT;
@ -78,7 +77,7 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
@Autowired
private ISearchIncludeDao mySearchIncludeDao;
@Autowired
private PlatformTransactionManager myTxManager;
private IHapiTransactionService myTransactionService;
@Autowired
private JpaStorageSettings myStorageSettings;
@ -87,45 +86,49 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
myCutoffSlack = theCutoffSlack;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Search save(Search theSearch) {
Search newSearch = mySearchDao.save(theSearch);
return newSearch;
public Search save(Search theSearch, RequestPartitionId theRequestPartitionId) {
return myTransactionService
.withSystemRequestOnPartition(theRequestPartitionId)
.execute(() -> mySearchDao.save(theSearch));
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public Optional<Search> fetchByUuid(String theUuid) {
public Optional<Search> fetchByUuid(String theUuid, RequestPartitionId theRequestPartitionId) {
Validate.notBlank(theUuid);
return mySearchDao.findByUuidAndFetchIncludes(theUuid);
return myTransactionService
.withSystemRequestOnPartition(theRequestPartitionId)
.execute(() -> mySearchDao.findByUuidAndFetchIncludes(theUuid));
}
void setSearchDaoForUnitTest(ISearchDao theSearchDao) {
mySearchDao = theSearchDao;
}
void setTxManagerForUnitTest(PlatformTransactionManager theTxManager) {
myTxManager = theTxManager;
void setTransactionServiceForUnitTest(IHapiTransactionService theTransactionService) {
myTransactionService = theTransactionService;
}
@Override
public Optional<Search> tryToMarkSearchAsInProgress(Search theSearch) {
public Optional<Search> tryToMarkSearchAsInProgress(Search theSearch, RequestPartitionId theRequestPartitionId) {
ourLog.trace("Going to try to change search status from {} to {}", theSearch.getStatus(), SearchStatusEnum.LOADING);
try {
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txTemplate.afterPropertiesSet();
return txTemplate.execute(t -> {
Search search = mySearchDao.findById(theSearch.getId()).orElse(theSearch);
if (search.getStatus() != SearchStatusEnum.PASSCMPLET) {
throw new IllegalStateException(Msg.code(1167) + "Can't change to LOADING because state is " + search.getStatus());
}
search.setStatus(SearchStatusEnum.LOADING);
Search newSearch = mySearchDao.save(search);
return Optional.of(newSearch);
});
return myTransactionService
.withSystemRequest()
.withRequestPartitionId(theRequestPartitionId)
.withPropagation(Propagation.REQUIRES_NEW)
.execute(t -> {
Search search = mySearchDao.findById(theSearch.getId()).orElse(theSearch);
if (search.getStatus() != SearchStatusEnum.PASSCMPLET) {
throw new IllegalStateException(Msg.code(1167) + "Can't change to LOADING because state is " + search.getStatus());
}
search.setStatus(SearchStatusEnum.LOADING);
Search newSearch = mySearchDao.save(search);
return Optional.of(newSearch);
});
} catch (Exception e) {
ourLog.warn("Failed to activate search: {}", e.toString());
ourLog.trace("Failed to activate search", e);
@ -135,6 +138,8 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
@Override
public Optional<Search> findCandidatesForReuse(String theResourceType, String theQueryString, Instant theCreatedAfter, RequestPartitionId theRequestPartitionId) {
HapiTransactionService.requireTransaction();
String queryString = Search.createSearchQueryStringForStorage(theQueryString, theRequestPartitionId);
int hashCode = queryString.hashCode();
@ -151,9 +156,10 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
return Optional.empty();
}
@Transactional(propagation = Propagation.NEVER)
@Override
public void pollForStaleSearchesAndDeleteThem() {
public void pollForStaleSearchesAndDeleteThem(RequestPartitionId theRequestPartitionId) {
HapiTransactionService.noTransactionAllowed();
if (!myStorageSettings.isExpireSearchResults()) {
return;
}
@ -170,38 +176,49 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
ourLog.debug("Searching for searches which are before {}", cutoff);
TransactionTemplate tt = new TransactionTemplate(myTxManager);
// Mark searches as deleted if they should be
final Slice<Long> toMarkDeleted = tt.execute(theStatus ->
mySearchDao.findWhereCreatedBefore(cutoff, new Date(), PageRequest.of(0, ourMaximumSearchesToCheckForDeletionCandidacy))
);
final Slice<Long> toMarkDeleted = myTransactionService
.withSystemRequestOnPartition(theRequestPartitionId)
.execute(theStatus ->
mySearchDao.findWhereCreatedBefore(cutoff, new Date(), PageRequest.of(0, ourMaximumSearchesToCheckForDeletionCandidacy))
);
assert toMarkDeleted != null;
for (final Long nextSearchToDelete : toMarkDeleted) {
ourLog.debug("Deleting search with PID {}", nextSearchToDelete);
tt.execute(t -> {
mySearchDao.updateDeleted(nextSearchToDelete, true);
return null;
});
myTransactionService
.withSystemRequest()
.withRequestPartitionId(theRequestPartitionId)
.execute(t -> {
mySearchDao.updateDeleted(nextSearchToDelete, true);
return null;
});
}
// Delete searches that are marked as deleted
final Slice<Long> toDelete = tt.execute(theStatus ->
mySearchDao.findDeleted(PageRequest.of(0, ourMaximumSearchesToCheckForDeletionCandidacy))
);
final Slice<Long> toDelete = myTransactionService
.withSystemRequestOnPartition(theRequestPartitionId)
.execute(theStatus ->
mySearchDao.findDeleted(PageRequest.of(0, ourMaximumSearchesToCheckForDeletionCandidacy))
);
assert toDelete != null;
for (final Long nextSearchToDelete : toDelete) {
ourLog.debug("Deleting search with PID {}", nextSearchToDelete);
tt.execute(t -> {
deleteSearch(nextSearchToDelete);
return null;
});
myTransactionService
.withSystemRequest()
.withRequestPartitionId(theRequestPartitionId)
.execute(t -> {
deleteSearch(nextSearchToDelete);
return null;
});
}
int count = toDelete.getContent().size();
if (count > 0) {
if (ourLog.isDebugEnabled() || HapiSystemProperties.isTestModeEnabled()) {
Long total = tt.execute(t -> mySearchDao.count());
Long total = myTransactionService
.withSystemRequest()
.withRequestPartitionId(theRequestPartitionId)
.execute(t -> mySearchDao.count());
ourLog.debug("Deleted {} searches, {} remaining", count, total);
}
}

View File

@ -34,7 +34,7 @@ public interface ISearchCacheSvc {
* @param theSearch The search to store
* @return Returns a copy of the search as it was saved. Callers should use the returned Search object for any further processing.
*/
Search save(Search theSearch);
Search save(Search theSearch, RequestPartitionId theRequestPartitionId);
/**
* Fetch a search using its UUID. The search should be fully loaded when it is returned (i.e. includes are fetched, so that access to its
@ -43,7 +43,7 @@ public interface ISearchCacheSvc {
* @param theUuid The search UUID
* @return The search if it exists
*/
Optional<Search> fetchByUuid(String theUuid);
Optional<Search> fetchByUuid(String theUuid, RequestPartitionId theRequestPartitionId);
/**
* TODO: this is perhaps an inappropriate responsibility for this service
@ -59,7 +59,7 @@ public interface ISearchCacheSvc {
* succeeded in marking it). If the search doesn't exist or some other error occurred, an exception will be thrown
* instead of {@link Optional#empty()}
*/
Optional<Search> tryToMarkSearchAsInProgress(Search theSearch);
Optional<Search> tryToMarkSearchAsInProgress(Search theSearch, RequestPartitionId theRequestPartitionId);
/**
* Look for any existing searches matching the given resource type and query string.
@ -82,5 +82,5 @@ public interface ISearchCacheSvc {
* if they have some other mechanism for expiring stale results other than manually looking for them
* and deleting them.
*/
void pollForStaleSearchesAndDeleteThem();
void pollForStaleSearchesAndDeleteThem(RequestPartitionId theRequestPartitionId);
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -134,7 +134,6 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
mySearchBuilderFactory,
mySynchronousSearchSvc,
myPersistedJpaBundleProviderFactory,
myPartitionHelperSvc,
null, // search param registry
mySearchStrategyFactory,
myExceptionSvc,
@ -187,7 +186,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
when(mySearchBuilder.createQuery(any(), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
when(mySearchCacheSvc.save(any())).thenAnswer(t -> {
when(mySearchCacheSvc.save(any(), any())).thenAnswer(t -> {
Search search = t.getArgument(0, Search.class);
myCurrentSearch = search;
return search;
@ -207,7 +206,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
assertEquals("799", resources.get(789).getIdElement().getValueAsString());
ArgumentCaptor<Search> searchCaptor = ArgumentCaptor.forClass(Search.class);
verify(mySearchCacheSvc, atLeastOnce()).save(searchCaptor.capture());
verify(mySearchCacheSvc, atLeastOnce()).save(searchCaptor.capture(), any());
assertEquals(790, allResults.size());
assertEquals(10, allResults.get(0).getId());
@ -224,10 +223,10 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
myCurrentSearch.setStatus(SearchStatusEnum.PASSCMPLET);
myCurrentSearch.setNumFound(10);
when(mySearchCacheSvc.fetchByUuid(any())).thenAnswer(t -> Optional.ofNullable(myCurrentSearch));
when(mySearchCacheSvc.fetchByUuid(any(), any())).thenAnswer(t -> Optional.ofNullable(myCurrentSearch));
when(mySearchCacheSvc.tryToMarkSearchAsInProgress(any())).thenAnswer(t -> {
when(mySearchCacheSvc.fetchByUuid(any())).thenAnswer(t2 -> Optional.empty());
when(mySearchCacheSvc.tryToMarkSearchAsInProgress(any(), any())).thenAnswer(t -> {
when(mySearchCacheSvc.fetchByUuid(any(), any())).thenAnswer(t2 -> Optional.empty());
return Optional.empty();
});
@ -248,7 +247,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
myCurrentSearch.setStatus(SearchStatusEnum.PASSCMPLET);
myCurrentSearch.setNumFound(10);
when(mySearchCacheSvc.fetchByUuid(any())).thenAnswer(t -> {
when(mySearchCacheSvc.fetchByUuid(any(), any())).thenAnswer(t -> {
sleepAtLeast(100);
return Optional.ofNullable(myCurrentSearch);
});
@ -373,7 +372,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
List<JpaPid> pids = createPidSequence(800);
IResultIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
when(mySearchCacheSvc.save(any())).thenAnswer(t -> {
when(mySearchCacheSvc.save(any(), any())).thenAnswer(t -> {
ourLog.info("Saving search");
return t.getArgument(0, Search.class);
});
@ -386,7 +385,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
assertEquals(790, result.size());
ArgumentCaptor<Search> searchCaptor = ArgumentCaptor.forClass(Search.class);
verify(mySearchCacheSvc, atLeast(1)).save(searchCaptor.capture());
verify(mySearchCacheSvc, atLeast(1)).save(searchCaptor.capture(), any());
Search search = searchCaptor.getValue();
assertEquals(SearchTypeEnum.SEARCH, search.getSearchType());
@ -450,7 +449,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
search.setStatus(SearchStatusEnum.LOADING);
search.setSearchParameterMap(new SearchParameterMap());
when(mySearchCacheSvc.fetchByUuid(eq(uuid))).thenReturn(Optional.of(search));
when(mySearchCacheSvc.fetchByUuid(eq(uuid), any())).thenReturn(Optional.of(search));
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
PersistedJpaBundleProvider provider;
@ -567,7 +566,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
search.setStatus(SearchStatusEnum.FINISHED);
search.setNumFound(100);
search.setTotalCount(100);
when(mySearchCacheSvc.fetchByUuid(eq("0000-1111"))).thenReturn(Optional.of(search));
when(mySearchCacheSvc.fetchByUuid(eq("0000-1111"), any())).thenReturn(Optional.of(search));
when(mySearchResultCacheSvc.fetchResultPids(any(), anyInt(), anyInt(), any(), any())).thenReturn(null);
@ -594,9 +593,9 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
search.setStatus(SearchStatusEnum.PASSCMPLET);
search.setNumFound(5);
search.setSearchParameterMap(new SearchParameterMap());
when(mySearchCacheSvc.fetchByUuid(eq("0000-1111"))).thenReturn(Optional.of(search));
when(mySearchCacheSvc.fetchByUuid(eq("0000-1111"), any())).thenReturn(Optional.of(search));
when(mySearchCacheSvc.tryToMarkSearchAsInProgress(any())).thenAnswer(t -> {
when(mySearchCacheSvc.tryToMarkSearchAsInProgress(any(), any())).thenAnswer(t -> {
search.setStatus(SearchStatusEnum.LOADING);
return Optional.of(search);
});

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4281,7 +4281,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
search.setStatus(SearchStatusEnum.FAILED);
search.setFailureCode(500);
search.setFailureMessage("FOO");
mySearchCacheSvc.save(search);
mySearchCacheSvc.save(search, RequestPartitionId.defaultPartition());
});
IBundleProvider results = myEncounterDao.search(map);

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
@ -85,7 +86,7 @@ public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test {
assertEquals(30, mySearchResultDao.count());
});
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem();
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem(RequestPartitionId.allPartitions());
runInTransaction(()->{
// We should delete up to 10, but 3 don't get deleted since they have too many results to delete in one pass
assertEquals(13, mySearchDao.count());
@ -94,7 +95,7 @@ public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test {
assertEquals(15, mySearchResultDao.count());
});
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem();
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem(RequestPartitionId.allPartitions());
runInTransaction(()->{
// Once again we attempt to delete 10, but the first 3 don't get deleted and still remain
// (total is 6 because 3 weren't deleted, and they blocked another 3 that might have been)
@ -103,7 +104,7 @@ public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test {
assertEquals(0, mySearchResultDao.count());
});
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem();
myDatabaseCacheSvc.pollForStaleSearchesAndDeleteThem(RequestPartitionId.allPartitions());
runInTransaction(()->{
assertEquals(0, mySearchDao.count());
assertEquals(0, mySearchDao.countDeleted());

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 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%
*/
package ca.uhn.fhir.jpa.test;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 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%
*/
package ca.uhn.fhir.jpa.test;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.search.cache;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.tx.NonTransactionalHapiTransactionService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import org.hibernate.HibernateException;
@ -9,7 +11,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.Optional;
@ -30,14 +31,11 @@ public class DatabaseSearchCacheSvcImplTest {
@Mock
private ISearchDao mySearchDao;
@Mock
private PlatformTransactionManager myTxManager;
@BeforeEach
public void before() {
mySvc = new DatabaseSearchCacheSvcImpl();
mySvc.setSearchDaoForUnitTest(mySearchDao);
mySvc.setTxManagerForUnitTest(myTxManager);
mySvc.setTransactionServiceForUnitTest(new NonTransactionalHapiTransactionService());
}
@Test
@ -48,7 +46,7 @@ public class DatabaseSearchCacheSvcImplTest {
when(mySearchDao.save(any())).thenReturn(updated);
Search search = new Search();
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search);
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search, RequestPartitionId.allPartitions());
assertTrue(outcome.isPresent());
verify(mySearchDao, times(1)).save(any());
@ -63,7 +61,7 @@ public class DatabaseSearchCacheSvcImplTest {
when(mySearchDao.save(any())).thenThrow(new HibernateException("FOO"));
Search search = new Search();
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search);
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search, RequestPartitionId.allPartitions());
assertFalse(outcome.isPresent());
verify(mySearchDao, times(1)).save(any());
}
@ -75,7 +73,7 @@ public class DatabaseSearchCacheSvcImplTest {
when(mySearchDao.findById(any())).thenReturn(Optional.of(updated));
Search search = new Search();
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search);
Optional<Search> outcome = mySvc.tryToMarkSearchAsInProgress(search, RequestPartitionId.allPartitions());
assertFalse(outcome.isPresent());
verify(mySearchDao, never()).save(any());
}

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -44,6 +44,11 @@
<artifactId>httpcore</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
</dependency>
<!--
Spring is added as an optional dependency just so that it
can be used for CORS

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.util.NarrativeUtil;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.StopWatch;
@ -800,28 +801,11 @@ public class ResponseHighlighterInterceptor {
}
/*
* FHIR only allows a pretty restricted set of HTML tags and attributes, in order
* to avoid any risk of injection attacks. If anything that isn't explicitly allowed
* by FHIR is present in the narrative we won't render it and instead we'll explain
* what validation problems we found.
* Sanitize the narrative so that it's safe to render (strip any
* links, potentially unsafe CSS, etc.)
*/
if (xhtmlNode != null) {
List<String> errors = new ArrayList<>();
Validate.isTrue(xhtmlNode.getName() == null);
xhtmlNode.getFirstElement().validate(errors, "", true, false, false);
if (errors.size() > 0) {
StringBuilder errorNarrative = new StringBuilder();
errorNarrative.append("Can not render narrative due to validation errors:");
errorNarrative.append("<ul>");
errors.forEach(next -> {
errorNarrative.append("<li>");
errorNarrative.append(sanitizeUrlPart(next));
errorNarrative.append("</li>");
});
errorNarrative.append("</ul>");
return errorNarrative.toString();
}
xhtmlNode = NarrativeUtil.sanitize(xhtmlNode);
return xhtmlNode.getValueAsString();
}

View File

@ -0,0 +1,91 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 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%
*/
package ca.uhn.fhir.rest.server.util;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
public class NarrativeUtil {
/**
* Non instantiable
*/
private NarrativeUtil() {
super();
}
/**
* This method accepts an Xhtml (generally a narrative) and sanitizes it,
* removing unsafe elements. This method leverages the
* <a href="https://github.com/OWASP/java-html-sanitizer/blob/master/pom.xml">OWASP Java HTML Sanitizer</a>
* to perform this task. The policy allows the following:
* <ul>
* <li>Block tags are allowed</li>
* <li>Tables are allowed</li>
* <li>Basic styles are allowed but any styles considered unsafe are removed from the document (e.g. any style declarations that could be used to load external content)</li>
* <li>Attributes considered safe are allowed</li>
* <li>Any links (&lta href="....") are removed although any text inside the link is retained</li>
* <li>All other elements and attributes are removed</li>
* </ul>
*/
public static String sanitize(String theHtml) {
XhtmlNode node = new XhtmlNode();
node.setValueAsString(theHtml);
return sanitize(node).getValueAsString();
}
/**
* This method accepts an Xhtml (generally a narrative) and sanitizes it,
* removing unsafe elements. This method leverages the
* <a href="https://github.com/OWASP/java-html-sanitizer/blob/master/pom.xml">OWASP Java HTML Sanitizer</a>
* to perform this task. The policy allows the following:
* <ul>
* <li>Block tags are allowed</li>
* <li>Tables are allowed</li>
* <li>Basic styles are allowed but any styles considered unsafe are removed from the document (e.g. any style declarations that could be used to load external content)</li>
* <li>Attributes considered safe are allowed</li>
* <li>Any links (&lta href="....") are removed although any text inside the link is retained</li>
* <li>All other elements and attributes are removed</li>
* </ul>
*/
public static XhtmlNode sanitize(XhtmlNode theNode) {
String html = theNode.getValueAsString();
PolicyFactory idPolicy = new HtmlPolicyBuilder()
.allowAttributes("id").globally()
.toFactory();
PolicyFactory policy = Sanitizers.FORMATTING
.and(Sanitizers.BLOCKS)
.and(Sanitizers.TABLES)
.and(Sanitizers.STYLES)
.and(idPolicy);
String safeHTML = policy.sanitize(html);
XhtmlNode retVal = new XhtmlNode();
retVal.setValueAsString(safeHTML);
return retVal;
}
}

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.rest.server.util;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class NarrativeUtilTest {
@ParameterizedTest
@CsvSource({
"<div><SPAN ID=\"foo\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
"<div><span id=\"foo\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
"<div><SPAN ONCLICK=\"hello()\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><span onclick=\"hello()\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><a href=\"http://goodbye\">hello</a></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><table><tr><td>hello</td></tr></table></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><table><tbody><tr><td>hello</td></tr></tbody></table></div>",
"<div><span style=\"font-size: 100px;\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span style=\"font-size:100px\">hello</span></div>",
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"hello , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"empty , null",
"null , null"
})
public void testValidateIsCaseInsensitive(String theHtml, String theExpected) {
String output = NarrativeUtil.sanitize(fixNull(theHtml));
assertEquals(fixNull(theExpected), output);
}
private String fixNull(String theExpected) {
if ("null".equals(theExpected)) {
return null;
}
if ("empty".equals(theExpected)) {
return "";
}
return theExpected;
}
}

View File

@ -7,7 +7,8 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -20,7 +21,8 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>

View File

@ -7,7 +7,8 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -7,7 +7,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -43,6 +43,6 @@ public interface ISearchCoordinatorSvc<T extends IResourcePersistentId> {
* Fetch the total number of search results for the given currently executing search, if one is currently executing and
* the total is known. Will return empty otherwise
*/
Optional<Integer> getSearchTotal(String theUuid, @Nullable RequestDetails theRequestDetails);
Optional<Integer> getSearchTotal(String theUuid, @Nullable RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId);
}

View File

@ -54,6 +54,22 @@ public interface IHapiTransactionService {
*/
IExecutionBuilder withSystemRequest();
/**
* Fluent builder for internal system requests with no external
* {@link RequestDetails} associated and a pre-specified partition ID.
* This method is sugar for
* <pre>
* withSystemRequest()
* .withRequestPartitionId(thePartitionId);
* </pre>
*
* @since 6.6.0
*/
default IExecutionBuilder withSystemRequestOnPartition(RequestPartitionId theRequestPartitionId) {
return withSystemRequest()
.withRequestPartitionId(theRequestPartitionId);
}
/**
* @deprecated It is highly recommended to use {@link #withRequest(RequestDetails)} instead of this method, for increased visibility.
*/

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -990,8 +990,7 @@ public class ResponseHighlightingInterceptorTest {
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = ourClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
assertThat(resp, not(containsString("<thead><tr><th>Header1</th><th>Header2</th></tr></thead>")));
assertThat(resp, containsString("Error at div/table: Found attribute table.onclick in a resource"));
assertThat(resp, containsString("<table><thead><tr><th>Header1</th>"));
}
}

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.client.impl.GenericClient;
import ca.uhn.fhir.rest.server.util.NarrativeUtil;
import ca.uhn.fhir.to.model.HomeRequest;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.ExtensionConstants;
@ -522,7 +523,7 @@ public class BaseController {
theModelMap.put("resultBodyIsLong", resultBodyText.length() > 1000);
theModelMap.put("requestHeaders", requestHeaders);
theModelMap.put("responseHeaders", responseHeaders);
theModelMap.put("narrative", narrativeString);
theModelMap.put("narrative", NarrativeUtil.sanitize(narrativeString));
theModelMap.put("latencyMs", theLatency);
theModelMap.put("config", myConfig);

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,8 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<description>An open-source implementation of the FHIR specification in Java.</description>
<url>https://hapifhir.io</url>
@ -1059,6 +1060,11 @@
<artifactId>caffeine</artifactId>
<version>${caffeine_version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20211018.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>

View File

@ -6,7 +6,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,8 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.5.10-SNAPSHOT</version>
<version>6.5.11-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>