From 6f79f6c5e2a741adb8075d01cc75ab36ca47da09 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 9 Mar 2016 15:34:49 -0500 Subject: [PATCH] Keep the complete resource history in the history table, including the current version --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 505 +++++-------- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 170 ++--- .../main/java/ca/uhn/fhir/jpa/dao/IDao.java | 33 +- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 531 ++++++-------- .../dao/data/IResourceHistoryTableDao.java | 106 +++ .../fhir/jpa/dao/data/IResourceTableDao.java | 2 +- .../ca/uhn/fhir/jpa/dao/data/ISearchDao.java | 7 +- .../fhir/jpa/entity/ResourceHistoryTable.java | 29 +- .../ca/uhn/fhir/jpa/entity/ResourceTable.java | 17 +- .../java/ca/uhn/fhir/jpa/entity/Search.java | 63 +- .../uhn/fhir/jpa/entity/SearchTypeEnum.java | 9 + .../ca/uhn/fhir/jpa/entity/TagDefinition.java | 14 + .../search/DatabaseBackedPagingProvider.java | 9 +- .../search/PersistedJpaBundleProvider.java | 246 +++++++ .../uhn/fhir/jpa/config/TestDstu1Config.java | 2 +- .../fhir/jpa/dao/FhirSystemDaoDstu1Test.java | 20 +- .../dao/dstu2/FhirResourceDaoDstu2Test.java | 406 +++++------ .../jpa/dao/dstu2/FhirSystemDaoDstu2Test.java | 6 +- .../dao/dstu3/FhirResourceDaoDstu3Test.java | 685 ++++++++++-------- .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 6 +- .../.classpath | 1 + .../src/main/java/.keep | 0 src/changes/changes.xml | 11 + 23 files changed, 1525 insertions(+), 1353 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchTypeEnum.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java create mode 100644 hapi-fhir-validation-resources-dstu3/src/main/java/.keep diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 6db60c8ff7c..8668a2b23a1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; /* * #%L * HAPI FHIR JPA Server @@ -27,7 +28,6 @@ import java.text.Normalizer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -35,6 +35,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.NoResultException; @@ -66,14 +67,8 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; -import com.google.common.base.Function; import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.ConfigurationException; @@ -83,6 +78,9 @@ import ca.uhn.fhir.context.RuntimeChildResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; +import ca.uhn.fhir.jpa.dao.data.ISearchDao; +import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.entity.BaseHasResource; import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; @@ -101,10 +99,12 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; import ca.uhn.fhir.jpa.entity.ResourceLink; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTag; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.util.DeleteConflict; -import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IResource; @@ -151,10 +151,8 @@ public abstract class BaseHapiFhirDao implements IDao { public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L); public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L); public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; - public static final String OO_SEVERITY_ERROR = "error"; public static final String OO_SEVERITY_INFO = "information"; - public static final String OO_SEVERITY_WARN = "warning"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); @@ -204,10 +202,19 @@ public abstract class BaseHapiFhirDao implements IDao { @Autowired private List> myResourceDaos; + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; + private Map, IFhirResourceDao> myResourceTypeToDao; + @Autowired + private ISearchDao mySearchDao; + private ISearchParamExtractor mySearchParamExtractor; + @Autowired + private ISearchResultDao mySearchResultDao; + protected void createForcedIdIfNeeded(ResourceTable entity, IIdType id) { if (id.isEmpty() == false && id.hasIdPart()) { if (isValidPid(id)) { @@ -269,19 +276,22 @@ public abstract class BaseHapiFhirDao implements IDao { String typeString = nextValue.getReferenceElement().getResourceType(); if (isBlank(typeString)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextValue.getReferenceElement().getValue()); + throw new InvalidRequestException( + "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextValue.getReferenceElement().getValue()); } RuntimeResourceDefinition resourceDefinition; try { resourceDefinition = getContext().getResourceDefinition(typeString); } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextValue.getReferenceElement().getValue()); + throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + + nextValue.getReferenceElement().getValue()); } Class type = resourceDefinition.getImplementingClass(); String id = nextValue.getReferenceElement().getIdPart(); if (StringUtils.isBlank(id)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextValue.getReferenceElement().getValue()); + throw new InvalidRequestException( + "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextValue.getReferenceElement().getValue()); } IFhirResourceDao dao = getDao(type); @@ -311,7 +321,8 @@ public abstract class BaseHapiFhirDao implements IDao { } if (!typeString.equals(target.getResourceType())) { - throw new UnprocessableEntityException("Resource contains reference to " + nextValue.getReferenceElement().getValue() + " but resource with ID " + nextValue.getReferenceElement().getIdPart() + " is actually of type " + target.getResourceType()); + throw new UnprocessableEntityException("Resource contains reference to " + nextValue.getReferenceElement().getValue() + " but resource with ID " + + nextValue.getReferenceElement().getIdPart() + " is actually of type " + target.getResourceType()); } /* @@ -364,15 +375,15 @@ public abstract class BaseHapiFhirDao implements IDao { return retVal; } - protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); - } - // @Override // public void setResourceDaos(List> theResourceDaos) { // myResourceDaos = theResourceDaos; // } + protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); + } + protected Set extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource); } @@ -501,6 +512,7 @@ public abstract class BaseHapiFhirDao implements IDao { return myConfig; } + @Override public FhirContext getContext() { return myContext; } @@ -608,90 +620,48 @@ public abstract class BaseHapiFhirDao implements IDao { } protected IBundleProvider history(String theResourceName, Long theId, Date theSince) { - final List tuples = new ArrayList(); - final InstantDt end = createHistoryToTimestamp(); + String resourceName = defaultIfBlank(theResourceName, null); - StopWatch timer = new StopWatch(); + Search search = new Search(); + search.setCreated(new Date()); + search.setLastUpdated(null, theSince); + search.setUuid(UUID.randomUUID().toString()); + search.setResourceType(resourceName); + search.setResourceId(theId); + search.setSearchType(SearchTypeEnum.HISTORY); - int limit = 10000; - - // Get list of IDs - searchHistoryCurrentVersion(theResourceName, theId, theSince, end.getValue(), limit, tuples); - ourLog.info("Retrieved {} history IDs from current versions in {} ms", tuples.size(), timer.getMillisAndRestart()); - - searchHistoryHistory(theResourceName, theId, theSince, end.getValue(), limit, tuples); - ourLog.info("Retrieved {} history IDs from previous versions in {} ms", tuples.size(), timer.getMillisAndRestart()); - - // Sort merged list - Collections.sort(tuples, Collections.reverseOrder()); - assert tuples.size() < 2 || !tuples.get(tuples.size() - 2).getUpdated().before(tuples.get(tuples.size() - 1).getUpdated()) : tuples.toString(); - - return new IBundleProvider() { - - @Override - public InstantDt getPublished() { - return end; + if (theSince != null) { + if (resourceName == null) { + search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince)); + } else if (theId == null) { + search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName, theSince)); + } else { + search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId, theSince)); } - - @Override - public List getResources(final int theFromIndex, final int theToIndex) { - final StopWatch grTimer = new StopWatch(); - TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); - return template.execute(new TransactionCallback>() { - @Override - public List doInTransaction(TransactionStatus theStatus) { - List resEntities = Lists.newArrayList(); - - List tupleSubList = tuples.subList(theFromIndex, theToIndex); - searchHistoryCurrentVersion(tupleSubList, resEntities); - ourLog.info("Loaded history from current versions in {} ms", grTimer.getMillisAndRestart()); - - searchHistoryHistory(tupleSubList, resEntities); - ourLog.info("Loaded history from previous versions in {} ms", grTimer.getMillisAndRestart()); - - Collections.sort(resEntities, new Comparator() { - @Override - public int compare(BaseHasResource theO1, BaseHasResource theO2) { - return theO2.getUpdated().getValue().compareTo(theO1.getUpdated().getValue()); - } - }); - - int grLimit = theToIndex - theFromIndex; - if (resEntities.size() > grLimit) { - resEntities = resEntities.subList(0, grLimit); - } - - ArrayList retVal = new ArrayList(); - for (BaseHasResource next : resEntities) { - RuntimeResourceDefinition type; - try { - type = myContext.getResourceDefinition(next.getResourceType()); - } catch (DataFormatException e) { - if (next.getFhirVersion() != getContext().getVersion().getVersion()) { - ourLog.info("Ignoring history resource of type[{}] because it is not compatible with version[{}]", next.getResourceType(), getContext().getVersion().getVersion()); - continue; - } - throw e; - } - IBaseResource resource = toResource(type.getImplementingClass(), next, true); - retVal.add(resource); - } - return retVal; - } - }); + } else { + if (resourceName == null) { + search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes()); + } else if (theId == null) { + search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName)); + } else { + search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId)); } + } + + search = mySearchDao.save(search); - @Override - public Integer preferredPageSize() { - return null; - } + return new PersistedJpaBundleProvider(search.getUuid(), this); + } - @Override - public int size() { - return tuples.size(); - } - }; + @Override + public void injectDependenciesIntoBundleProvider(PersistedJpaBundleProvider theProvider) { + theProvider.setContext(getContext()); + theProvider.setEntityManager(myEntityManager); + theProvider.setPlatformTransactionManager(myPlatformTransactionManager); + theProvider.setResourceHistoryTableDao(myResourceHistoryTableDao); + theProvider.setSearchDao(mySearchDao); + theProvider.setSearchResultDao(mySearchResultDao); } protected void notifyInterceptors(RestOperationTypeEnum operationType, ActionRequestDetails requestDetails) { @@ -808,8 +778,7 @@ public abstract class BaseHapiFhirDao implements IDao { } } else if (theForHistoryOperation) { /* - * If the create and update times match, this was when the resource was created so we should mark it as a POST. - * Otherwise, it's a PUT. + * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT. */ Date published = theEntity.getPublished().getValue(); Date updated = theEntity.getUpdated().getValue(); @@ -863,7 +832,7 @@ public abstract class BaseHapiFhirDao implements IDao { ResourceMetadataKeyEnum.PROFILES.put(res, profiles); } } - + return retVal; } @@ -879,8 +848,7 @@ public abstract class BaseHapiFhirDao implements IDao { } } else if (theForHistoryOperation) { /* - * If the create and update times match, this was when the resource was created so we should mark it as a POST. - * Otherwise, it's a PUT. + * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT. */ Date published = theEntity.getPublished().getValue(); Date updated = theEntity.getUpdated().getValue(); @@ -896,13 +864,14 @@ public abstract class BaseHapiFhirDao implements IDao { res.getMeta().getSecurity().clear(); res.getMeta().setLastUpdated(null); res.getMeta().setVersionId(null); - + populateResourceId(res, theEntity); res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); IDao.RESOURCE_PID.put(res, theEntity.getId()); Collection tags = theEntity.getTags(); + if (theEntity.isHasTags()) { for (BaseTag next : tags) { switch (next.getTag().getTagType()) { @@ -928,8 +897,7 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the - * first time. + * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the first time. * * @param theEntity * The entity being updated (Do not modify the entity! Undefined behaviour will occur!) @@ -943,8 +911,7 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the - * first time. + * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the first time. * * @param theEntity * The resource @@ -977,146 +944,6 @@ public abstract class BaseHapiFhirDao implements IDao { throw new NotImplementedException(""); } - private void searchHistoryCurrentVersion(List theTuples, List theRetVal) { - Collection tuples = Collections2.filter(theTuples, new com.google.common.base.Predicate() { - @Override - public boolean apply(HistoryTuple theInput) { - return theInput.isHistory() == false; - } - }); - Collection ids = Collections2.transform(tuples, new Function() { - @Override - public Long apply(HistoryTuple theInput) { - return theInput.getId(); - } - }); - if (ids.isEmpty()) { - return; - } - - CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createQuery(ResourceTable.class); - Root from = cq.from(ResourceTable.class); - cq.where(from.get("myId").in(ids)); - - cq.orderBy(builder.desc(from.get("myUpdated"))); - TypedQuery q = myEntityManager.createQuery(cq); - for (ResourceTable next : q.getResultList()) { - theRetVal.add(next); - } - } - - private void searchHistoryCurrentVersion(String theResourceName, Long theId, Date theSince, Date theEnd, Integer theLimit, List tuples) { - CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createTupleQuery(); - Root from = cq.from(ResourceTable.class); - cq.multiselect(from.get("myId").as(Long.class), from.get("myUpdated").as(Date.class)); - - List predicates = new ArrayList(); - if (theSince != null) { - Predicate low = builder.greaterThanOrEqualTo(from. get("myUpdated"), theSince); - predicates.add(low); - } - - Predicate high = builder.lessThan(from. get("myUpdated"), theEnd); - predicates.add(high); - - if (theResourceName != null) { - predicates.add(builder.equal(from.get("myResourceType"), theResourceName)); - } - if (theId != null) { - predicates.add(builder.equal(from.get("myId"), theId)); - } - - cq.where(builder.and(predicates.toArray(new Predicate[0]))); - - cq.orderBy(builder.desc(from.get("myUpdated"))); - TypedQuery q = myEntityManager.createQuery(cq); - if (theLimit != null && theLimit < myConfig.getHardSearchLimit()) { - q.setMaxResults(theLimit); - } else { - q.setMaxResults(myConfig.getHardSearchLimit()); - } - for (Tuple next : q.getResultList()) { - long id = next.get(0, Long.class); - Date updated = next.get(1, Date.class); - tuples.add(new HistoryTuple(false, updated, id)); - } - } - - private void searchHistoryHistory(List theTuples, List theRetVal) { - Collection tuples = Collections2.filter(theTuples, new com.google.common.base.Predicate() { - @Override - public boolean apply(HistoryTuple theInput) { - return theInput.isHistory() == true; - } - }); - Collection ids = Collections2.transform(tuples, new Function() { - @Override - public Long apply(HistoryTuple theInput) { - return (Long) theInput.getId(); - } - }); - if (ids.isEmpty()) { - return; - } - - ourLog.info("Retrieving {} history elements from ResourceHistoryTable", ids.size()); - - CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createQuery(ResourceHistoryTable.class); - Root from = cq.from(ResourceHistoryTable.class); - cq.where(from.get("myId").in(ids)); - - cq.orderBy(builder.desc(from.get("myUpdated"))); - TypedQuery q = myEntityManager.createQuery(cq); - for (ResourceHistoryTable next : q.getResultList()) { - theRetVal.add(next); - } - } - - private void searchHistoryHistory(String theResourceName, Long theResourceId, Date theSince, Date theEnd, Integer theLimit, List tuples) { - CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createTupleQuery(); - Root from = cq.from(ResourceHistoryTable.class); - cq.multiselect(from.get("myId").as(Long.class), from.get("myUpdated").as(Date.class)); - - List predicates = new ArrayList(); - if (theSince != null) { - Predicate low = builder.greaterThanOrEqualTo(from. get("myUpdated"), theSince); - predicates.add(low); - } - - Predicate high = builder.lessThan(from. get("myUpdated"), theEnd); - predicates.add(high); - - if (theResourceName != null) { - predicates.add(builder.equal(from.get("myResourceType"), theResourceName)); - } - if (theResourceId != null) { - predicates.add(builder.equal(from.get("myResourceId"), theResourceId)); - } - - cq.where(builder.and(predicates.toArray(new Predicate[0]))); - - cq.orderBy(builder.desc(from.get("myUpdated"))); - TypedQuery q = myEntityManager.createQuery(cq); - if (theLimit != null && theLimit < myConfig.getHardSearchLimit()) { - q.setMaxResults(theLimit); - } else { - q.setMaxResults(myConfig.getHardSearchLimit()); - } - for (Tuple next : q.getResultList()) { - Long id = next.get(0, Long.class); - Date updated = (Date) next.get(1); - tuples.add(new HistoryTuple(true, updated, id)); - } - } - - public void setConfig(DaoConfig theConfig) { - myConfig = theConfig; - } - // protected MetaDt toMetaDt(Collection tagDefinitions) { // MetaDt retVal = new MetaDt(); // for (TagDefinition next : tagDefinitions) { @@ -1135,6 +962,10 @@ public abstract class BaseHapiFhirDao implements IDao { // return retVal; // } + public void setConfig(DaoConfig theConfig) { + myConfig = theConfig; + } + @Autowired public void setContext(FhirContext theContext) { myContext = theContext; @@ -1162,15 +993,12 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * This method is called when an update to an existing resource detects that the resource supplied for update is - * missing a tag/profile/security label that the currently persisted resource holds. + * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds. *

- * The default implementation removes any profile declarations, but leaves tags and security labels in place. - * Subclasses may choose to override and change this behaviour. + * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour. *

*

- * See Updates to Tags, Profiles, and Security - * Labels for a description of the logic that the default behaviour folows. + * See Updates to Tags, Profiles, and Security Labels for a description of the logic that the default behaviour folows. *

* * @param theEntity @@ -1200,53 +1028,53 @@ public abstract class BaseHapiFhirDao implements IDao { return toResource(type.getImplementingClass(), theEntity, theForHistoryOperation); } - @Override - public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { - String resourceText = null; - switch (theEntity.getEncoding()) { - case JSON: - try { - resourceText = new String(theEntity.getResource(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new Error("Should not happen", e); - } - break; - case JSONC: - resourceText = GZipUtil.decompress(theEntity.getResource()); - break; - } - - IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); - R retVal; + @Override + public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { + String resourceText = null; + switch (theEntity.getEncoding()) { + case JSON: try { - retVal = parser.parseResource(theResourceType, resourceText); - } catch (Exception e) { - StringBuilder b = new StringBuilder(); - b.append("Failed to parse database resource["); - b.append(theResourceType); - b.append("/"); - b.append(theEntity.getIdDt().getIdPart()); - b.append(" (pid "); - b.append(theEntity.getId()); - b.append(", version "); - b.append(myContext.getVersion().getVersion()); - b.append("): "); - b.append(e.getMessage()); - String msg = b.toString(); - ourLog.error(msg, e); - throw new DataFormatException(msg, e); + resourceText = new String(theEntity.getResource(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Should not happen", e); } - - if (retVal instanceof IResource) { - IResource res = (IResource) retVal; - retVal = populateResourceMetadataHapi(theResourceType, theEntity, theForHistoryOperation, res); - } else { - IAnyResource res = (IAnyResource) retVal; - retVal = populateResourceMetadataRi(theResourceType, theEntity, theForHistoryOperation, res); - } - return retVal; + break; + case JSONC: + resourceText = GZipUtil.decompress(theEntity.getResource()); + break; } + IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); + R retVal; + try { + retVal = parser.parseResource(theResourceType, resourceText); + } catch (Exception e) { + StringBuilder b = new StringBuilder(); + b.append("Failed to parse database resource["); + b.append(theResourceType); + b.append("/"); + b.append(theEntity.getIdDt().getIdPart()); + b.append(" (pid "); + b.append(theEntity.getId()); + b.append(", version "); + b.append(myContext.getVersion().getVersion()); + b.append("): "); + b.append(e.getMessage()); + String msg = b.toString(); + ourLog.error(msg, e); + throw new DataFormatException(msg, e); + } + + if (retVal instanceof IResource) { + IResource res = (IResource) retVal; + retVal = populateResourceMetadataHapi(theResourceType, theEntity, theForHistoryOperation, res); + } else { + IAnyResource res = (IAnyResource) retVal; + retVal = populateResourceMetadataRi(theResourceType, theEntity, theForHistoryOperation, res); + } + return retVal; + } + protected String toResourceName(Class theResourceType) { return myContext.getResourceDefinition(theResourceType).getName(); } @@ -1268,12 +1096,10 @@ public abstract class BaseHapiFhirDao implements IDao { } } - protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, Date theUpdateTime, RequestDetails theRequestDetails) { - return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true, theUpdateTime, theRequestDetails); - } - @SuppressWarnings("unchecked") - protected ResourceTable updateEntity(final IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime, RequestDetails theRequestDetails) { + protected ResourceTable updateEntity(final IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, + boolean theUpdateVersion, Date theUpdateTime, RequestDetails theRequestDetails) { + ourLog.info("Starting entity update"); /* * This should be the very first thing.. @@ -1284,7 +1110,8 @@ public abstract class BaseHapiFhirDao implements IDao { } String resourceType = myContext.getResourceDefinition(theResource).getName(); if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) { - throw new UnprocessableEntityException("Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); + throw new UnprocessableEntityException( + "Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); } } @@ -1292,10 +1119,15 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setPublished(theUpdateTime); } - if (theUpdateHistory) { - final ResourceHistoryTable historyEntry = theEntity.toHistory(); - myEntityManager.persist(historyEntry); - } +// if (theUpdateHistory) { +// Long existingId = theEntity.getId(); +// ResourceHistoryTable existingHistory = existingId != null ? myResourceHistoryTableDao.findForIdAndVersion(existingId, theEntity.getVersion()) : null; +// final ResourceHistoryTable historyEntry = theEntity.toHistory(existingHistory); +// +// ourLog.info("Saving history entry for update {}", historyEntry.getIdDt()); +// myResourceHistoryTableDao.save(historyEntry); +// +// } if (theUpdateVersion) { theEntity.setVersion(theEntity.getVersion() + 1); @@ -1384,9 +1216,8 @@ public abstract class BaseHapiFhirDao implements IDao { } /* - * Handle references within the resource that are match URLs, for example - * references like "Patient?identifier=foo". These match URLs are resolved - * and replaced with the ID of the matching resource. + * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the matching + * resource. */ if (myConfig.isAllowInlineMatchUrlReferences()) { FhirTerser terser = getContext().newTerser(); @@ -1425,12 +1256,11 @@ public abstract class BaseHapiFhirDao implements IDao { } } } - + links = extractResourceLinks(theEntity, theResource); /* - * If the existing resource already has links and those match links we still want, use them instead of - * removing them and re adding them + * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them */ for (Iterator existingLinkIter = existingResourceLinks.iterator(); existingLinkIter.hasNext();) { ResourceLink nextExisting = existingLinkIter.next(); @@ -1479,6 +1309,9 @@ public abstract class BaseHapiFhirDao implements IDao { } + /* + * Save the resource itself + */ if (theEntity.getId() == null) { myEntityManager.persist(theEntity); @@ -1494,6 +1327,19 @@ public abstract class BaseHapiFhirDao implements IDao { postUpdate(theEntity, (T) theResource); } + /* + * Create history entry + */ + if (theUpdateVersion) { + final ResourceHistoryTable historyEntry = theEntity.toHistory(null); + + ourLog.info("Saving history entry {}", historyEntry.getIdDt()); + myResourceHistoryTableDao.save(historyEntry); + } + + /* + * Indexing + */ if (thePerformIndexing) { for (ResourceIndexedSearchParamString next : paramsString) { @@ -1569,6 +1415,11 @@ public abstract class BaseHapiFhirDao implements IDao { return theEntity; } + protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, Date theUpdateTime, + RequestDetails theRequestDetails) { + return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true, theUpdateTime, theRequestDetails); + } + protected void validateDeleteConflictsEmptyOrThrowException(List theDeleteConflicts) { if (theDeleteConflicts.isEmpty()) { return; @@ -1576,24 +1427,25 @@ public abstract class BaseHapiFhirDao implements IDao { IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); for (DeleteConflict next : theDeleteConflicts) { - String msg = "Unable to delete " + next.getTargetId().toUnqualifiedVersionless().getValue() + " because at least one resource has a reference to this resource. First reference found was resource " + next.getTargetId().toUnqualifiedVersionless().getValue() + " in path " + String msg = "Unable to delete " + next.getTargetId().toUnqualifiedVersionless().getValue() + + " because at least one resource has a reference to this resource. First reference found was resource " + next.getTargetId().toUnqualifiedVersionless().getValue() + " in path " + next.getSourcePath(); OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing"); } throw new ResourceVersionConflictException("Delete failed because of constraint failure", oo); } - + /** - * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow - * the DAO to ensure that it is valid for persistence. By default, checks for the "subsetted" tag and rejects - * resources which have it. Subclasses should call the superclass implementation to preserve this check. + * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow the DAO to ensure that it is valid for persistence. By default, checks for the + * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check. * * @param theResource * The resource that is about to be persisted * @param theEntityToSave * TODO - * @param theRequestDetails TODO + * @param theRequestDetails + * TODO */ protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave, RequestDetails theRequestDetails) { Object tag = null; @@ -1604,10 +1456,10 @@ public abstract class BaseHapiFhirDao implements IDao { tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE); } } else { - IAnyResource res = (IAnyResource)theResource; + IAnyResource res = (IAnyResource) theResource; tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE); } - + if (tag != null) { throw new UnprocessableEntityException("Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data"); } @@ -1691,22 +1543,22 @@ public abstract class BaseHapiFhirDao implements IDao { b.append(characters.getData()).append(" "); } } - } + } } catch (Exception e) { throw new DataFormatException("Unable to convert DIV to string", e); } - + } return b.toString(); } private static List toBaseCodingList(List theSecurityLabels) { - ArrayList retVal = new ArrayList(theSecurityLabels.size()); - for (IBaseCoding next : theSecurityLabels) { - retVal.add((BaseCodingDt) next); + ArrayList retVal = new ArrayList(theSecurityLabels.size()); + for (IBaseCoding next : theSecurityLabels) { + retVal.add((BaseCodingDt) next); + } + return retVal; } - return retVal; -} static Long translateForcedIdToPid(IIdType theId, EntityManager entityManager) { if (isValidPid(theId)) { @@ -1820,7 +1672,8 @@ public abstract class BaseHapiFhirDao implements IDao { public static void validateResourceType(BaseHasResource theEntity, String theResourceName) { if (!theResourceName.equals(theEntity.getResourceType())) { - throw new ResourceNotFoundException("Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); + throw new ResourceNotFoundException( + "Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 6c825ae705f..0b209e782b1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -34,7 +34,6 @@ import java.util.Set; import javax.annotation.PostConstruct; import javax.persistence.NoResultException; -import javax.persistence.TemporalType; import javax.persistence.TypedQuery; import org.hl7.fhir.dstu3.model.IdType; @@ -55,6 +54,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.entity.BaseHasResource; @@ -73,7 +73,6 @@ import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.method.RestSearchParameterTypeEnum; @@ -387,113 +386,15 @@ public abstract class BaseHapiFhirResourceDao extends B ActionRequestDetails requestDetails = new ActionRequestDetails(theId, getResourceName(), getContext(), theRequestDetails); notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails); - final InstantDt end = createHistoryToTimestamp(); - final String resourceType = getContext().getResourceDefinition(myResourceType).getName(); + StopWatch w = new StopWatch(); - T currentTmp; - try { - BaseHasResource entity = readEntity(theId.toVersionless(), false); - validateResourceType(entity); - currentTmp = toResource(myResourceType, entity, true); - Date lastUpdated; - if (currentTmp instanceof IResource) { - lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) currentTmp).getValue(); - } else { - lastUpdated = ((IAnyResource) currentTmp).getMeta().getLastUpdated(); - } - if (lastUpdated.after(end.getValue())) { - currentTmp = null; - } - } catch (ResourceNotFoundException e) { - currentTmp = null; - } - - final T current = currentTmp; - - StringBuilder B = new StringBuilder(); - B.append("SELECT count(h) FROM ResourceHistoryTable h "); - B.append("WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE"); - B.append(" AND h.myUpdated < :END"); - B.append((theSince != null ? " AND h.myUpdated >= :SINCE" : "")); - String querySring = B.toString(); - - TypedQuery countQuery = myEntityManager.createQuery(querySring, Long.class); - countQuery.setParameter("PID", translateForcedIdToPid(theId)); - countQuery.setParameter("RESTYPE", resourceType); - countQuery.setParameter("END", end.getValue(), TemporalType.TIMESTAMP); - if (theSince != null) { - countQuery.setParameter("SINCE", theSince, TemporalType.TIMESTAMP); - } - int historyCount = countQuery.getSingleResult().intValue(); - - final int offset; - final int count; - if (current != null) { - count = historyCount + 1; - offset = 1; - } else { - offset = 0; - count = historyCount; - } - - if (count == 0) { - throw new ResourceNotFoundException(theId); - } - - return new IBundleProvider() { - - @Override - public InstantDt getPublished() { - return end; - } - - @Override - public List getResources(int theFromIndex, int theToIndex) { - List retVal = new ArrayList(); - if (theFromIndex == 0 && current != null) { - retVal.add(current); - } - - StringBuilder b = new StringBuilder(); - b.append("SELECT h FROM ResourceHistoryTable h WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE AND h.myUpdated < :END "); - b.append((theSince != null ? " AND h.myUpdated >= :SINCE" : "")); - b.append(" ORDER BY h.myUpdated DESC"); - TypedQuery q = myEntityManager.createQuery(b.toString(), ResourceHistoryTable.class); - q.setParameter("PID", translateForcedIdToPid(theId)); - q.setParameter("RESTYPE", resourceType); - q.setParameter("END", end.getValue(), TemporalType.TIMESTAMP); - if (theSince != null) { - q.setParameter("SINCE", theSince, TemporalType.TIMESTAMP); - } - - int firstResult = Math.max(0, theFromIndex - offset); - q.setFirstResult(firstResult); - - int maxResults = (theToIndex - theFromIndex) + 1; - q.setMaxResults(maxResults); - - List results = q.getResultList(); - for (ResourceHistoryTable next : results) { - if (retVal.size() == maxResults) { - break; - } - retVal.add(toResource(myResourceType, next, true)); - } - - return retVal; - } - - @Override - public Integer preferredPageSize() { - return null; - } - - @Override - public int size() { - return count; - } - }; + IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); + BaseHasResource entity = readEntity(id); + + IBundleProvider retVal = super.history(myResourceName, entity.getId(), theSince); + ourLog.info("Processed history on {} in {}ms", id, w.getMillisAndRestart()); + return retVal; } @Override @@ -508,6 +409,9 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; + @Override public MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) { // Notify interceptors @@ -520,6 +424,25 @@ public abstract class BaseHapiFhirResourceDao extends B throw new ResourceNotFoundException(theResourceId); } + ResourceTable latestVersion = readEntityLatestVersion(theResourceId); + if (latestVersion.getVersion() != entity.getVersion()) { + doMetaAdd(theMetaAdd, entity); + } else { + doMetaAdd(theMetaAdd, latestVersion); + + // Also update history entry + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion()); + doMetaAdd(theMetaAdd, history); + } + + ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() }); + + @SuppressWarnings("unchecked") + MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequestDetails); + return retVal; + } + + private void doMetaAdd(MT theMetaAdd, BaseHasResource entity) { List tags = toTagList(theMetaAdd); //@formatter:off @@ -546,11 +469,6 @@ public abstract class BaseHapiFhirResourceDao extends B //@formatter:on myEntityManager.merge(entity); - ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() }); - - @SuppressWarnings("unchecked") - MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequestDetails); - return retVal; } // @Override @@ -647,6 +565,27 @@ public abstract class BaseHapiFhirResourceDao extends B throw new ResourceNotFoundException(theResourceId); } + ResourceTable latestVersion = readEntityLatestVersion(theResourceId); + if (latestVersion.getVersion() != entity.getVersion()) { + doMetaDelete(theMetaDel, entity); + } else { + doMetaDelete(theMetaDel, latestVersion); + + // Also update history entry + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion()); + doMetaDelete(theMetaDel, history); + } + + myEntityManager.flush(); + + ourLog.info("Processed metaDeleteOperation on {} in {}ms", new Object[] { theResourceId.getValue(), w.getMillisAndRestart() }); + + @SuppressWarnings("unchecked") + MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequestDetails); + return retVal; + } + + private void doMetaDelete(MT theMetaDel, BaseHasResource entity) { List tags = toTagList(theMetaDel); //@formatter:off @@ -667,13 +606,6 @@ public abstract class BaseHapiFhirResourceDao extends B } myEntityManager.merge(entity); - myEntityManager.flush(); - - ourLog.info("Processed metaDeleteOperation on {} in {}ms", new Object[] { theResourceId.getValue(), w.getMillisAndRestart() }); - - @SuppressWarnings("unchecked") - MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequestDetails); - return retVal; } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java index 03ea9e975b3..ab9b6a6242a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java @@ -3,7 +3,9 @@ package ca.uhn.fhir.jpa.dao; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.entity.BaseHasResource; +import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum.ResourceMetadataKeySupportingAnyResource; @@ -33,30 +35,35 @@ public interface IDao { private static final long serialVersionUID = 1L; - @Override - public Long get(IResource theResource) { - return (Long) theResource.getResourceMetadata().get(RESOURCE_PID); - } - - @Override - public void put(IResource theResource, Long theObject) { - theResource.getResourceMetadata().put(RESOURCE_PID, theObject); - } - @Override public Long get(IAnyResource theResource) { return (Long) theResource.getUserData(RESOURCE_PID.name()); } + @Override + public Long get(IResource theResource) { + return (Long) theResource.getResourceMetadata().get(RESOURCE_PID); + } + @Override public void put(IAnyResource theResource, Long theObject) { theResource.setUserData(RESOURCE_PID.name(), theObject); } + + @Override + public void put(IResource theResource, Long theObject) { + theResource.getResourceMetadata().put(RESOURCE_PID, theObject); + } }; + FhirContext getContext(); + + /** + * Populate all of the runtime dependencies that a bundle provider requires in order to work + */ + void injectDependenciesIntoBundleProvider(PersistedJpaBundleProvider theProvider); + IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation); - - R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation); - + R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index af3ae6fea05..7737ce43300 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -42,7 +42,6 @@ import java.util.Set; import java.util.UUID; import javax.persistence.EntityManager; -import javax.persistence.NoResultException; import javax.persistence.Tuple; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; @@ -62,9 +61,6 @@ import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -93,8 +89,10 @@ import ca.uhn.fhir.jpa.entity.ResourceTag; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchInclude; import ca.uhn.fhir.jpa.entity.SearchResult; +import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -943,21 +941,6 @@ public class SearchBuilder { return retVal; } - private static List createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From from) { - List lastUpdatedPredicates = new ArrayList(); - if (theLastUpdated != null) { - if (theLastUpdated.getLowerBoundAsInstant() != null) { - Predicate predicateLower = builder.greaterThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()); - lastUpdatedPredicates.add(predicateLower); - } - if (theLastUpdated.getUpperBoundAsInstant() != null) { - Predicate predicateUpper = builder.lessThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()); - lastUpdatedPredicates.add(predicateUpper); - } - } - return lastUpdatedPredicates; - } - private Predicate createPredicateDate(CriteriaBuilder theBuilder, From theFrom, IQueryParameterType theParam) { Predicate p; if (theParam instanceof DateParam) { @@ -1307,7 +1290,7 @@ public class SearchBuilder { mySearchEntity.setCreated(new Date()); mySearchEntity.setTotalCount(-1); mySearchEntity.setPreferredPageSize(myParams.getCount()); - mySearchEntity.setEverythingMode(myParams.getEverythingMode()); + mySearchEntity.setSearchType(myParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH); mySearchEntity.setLastUpdated(myParams.getLastUpdated()); for (Include next : myParams.getIncludes()) { @@ -1329,7 +1312,7 @@ public class SearchBuilder { private IBundleProvider doReturnProvider() { if (myParams.isPersistResults()) { - return new BundleProviderPersisted(mySearchEntity.getUuid(), myPlatformTransactionManager, mySearchResultDao, myEntityManager, myContext, myCallingDao); + return new PersistedJpaBundleProvider(mySearchEntity.getUuid(), myCallingDao); } else { if (myPids == null) { return new SimpleBundleProvider(); @@ -1381,22 +1364,6 @@ public class SearchBuilder { doSetPids(resultList); } - private static List filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection thePids) { - CriteriaBuilder builder = theEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createQuery(Long.class); - Root from = cq.from(ResourceTable.class); - cq.select(from.get("myId").as(Long.class)); - - List lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from); - lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids)); - - cq.where(SearchBuilder.toArray(lastUpdatedPredicates)); - TypedQuery query = theEntityManager.createQuery(cq); - - List resultList = query.getResultList(); - return resultList; - } - private void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation) { EntityManager entityManager = myEntityManager; FhirContext context = myContext; @@ -1405,179 +1372,6 @@ public class SearchBuilder { loadResourcesByPid(theIncludePids, theResourceListToPopulate, theRevIncludedPids, theForHistoryOperation, entityManager, context, dao); } - private static void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, - EntityManager entityManager, FhirContext context, IDao theDao) { - if (theIncludePids.isEmpty()) { - return; - } - - Map position = new HashMap(); - for (Long next : theIncludePids) { - position.put(next, theResourceListToPopulate.size()); - theResourceListToPopulate.add(null); - } - - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createQuery(ResourceTable.class); - Root from = cq.from(ResourceTable.class); - cq.where(from.get("myId").in(theIncludePids)); - TypedQuery q = entityManager.createQuery(cq); - - for (ResourceTable next : q.getResultList()) { - Class resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); - IBaseResource resource = (IBaseResource) theDao.toResource(resourceType, next, theForHistoryOperation); - Integer index = position.get(next.getId()); - if (index == null) { - ourLog.warn("Got back unexpected resource PID {}", next.getId()); - continue; - } - - if (resource instanceof IResource) { - if (theRevIncludedPids.contains(next.getId())) { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE); - } else { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH); - } - } else { - if (theRevIncludedPids.contains(next.getId())) { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode()); - } else { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode()); - } - } - - theResourceListToPopulate.set(index, resource); - } - } - - /** - * THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet()) - * - * @param theLastUpdated - */ - private static HashSet loadReverseIncludes(FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, - DateRangeParam theLastUpdated) { - if (theMatches.size() == 0) { - return new HashSet(); - } - if (theRevIncludes == null || theRevIncludes.isEmpty()) { - return new HashSet(); - } - String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; - - Collection nextRoundMatches = theMatches; - HashSet allAdded = new HashSet(); - HashSet original = new HashSet(theMatches); - ArrayList includes = new ArrayList(theRevIncludes); - - int roundCounts = 0; - StopWatch w = new StopWatch(); - - boolean addedSomeThisRound; - do { - roundCounts++; - - HashSet pidsToInclude = new HashSet(); - Set nextRoundOmit = new HashSet(); - - for (Iterator iter = includes.iterator(); iter.hasNext();) { - Include nextInclude = iter.next(); - if (nextInclude.isRecurse() == false) { - iter.remove(); - } - - boolean matchAll = "*".equals(nextInclude.getValue()); - if (matchAll) { - String sql; - sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids)"; - TypedQuery q = theEntityManager.createQuery(sql, ResourceLink.class); - q.setParameter("target_pids", nextRoundMatches); - List results = q.getResultList(); - for (ResourceLink resourceLink : results) { - if (theReverseMode) { - // if (theEverythingModeEnum.isEncounter()) { - // if (resourceLink.getSourcePath().equals("Encounter.subject") || - // resourceLink.getSourcePath().equals("Encounter.patient")) { - // nextRoundOmit.add(resourceLink.getSourceResourcePid()); - // } - // } - pidsToInclude.add(resourceLink.getSourceResourcePid()); - } else { - pidsToInclude.add(resourceLink.getTargetResourcePid()); - } - } - } else { - - List paths; - if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { - paths = Collections.singletonList(nextInclude.getValue()); - } else { - String resType = nextInclude.getParamType(); - if (isBlank(resType)) { - continue; - } - RuntimeResourceDefinition def = theContext.getResourceDefinition(resType); - if (def == null) { - ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); - continue; - } - - String paramName = nextInclude.getParamName(); - RuntimeSearchParam param = isNotBlank(paramName) ? def.getSearchParam(paramName) : null; - if (param == null) { - ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); - continue; - } - - paths = param.getPathsSplit(); - } - - String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); - for (String nextPath : paths) { - String sql; - if (targetResourceType != null) { - sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type"; - } else { - sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; - } - TypedQuery q = theEntityManager.createQuery(sql, ResourceLink.class); - q.setParameter("src_path", nextPath); - q.setParameter("target_pids", nextRoundMatches); - if (targetResourceType != null) { - q.setParameter("target_resource_type", targetResourceType); - } - List results = q.getResultList(); - for (ResourceLink resourceLink : results) { - if (theReverseMode) { - pidsToInclude.add(resourceLink.getSourceResourcePid()); - } else { - pidsToInclude.add(resourceLink.getTargetResourcePid()); - } - } - } - } - } - - if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) { - pidsToInclude = new HashSet(filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude)); - } - for (Long next : pidsToInclude) { - if (original.contains(next) == false && allAdded.contains(next) == false) { - theMatches.add(next); - } - } - - pidsToInclude.removeAll(nextRoundOmit); - - addedSomeThisRound = allAdded.addAll(pidsToInclude); - nextRoundMatches = pidsToInclude; - } while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound); - - ourLog.info("Loaded {} {} in {} rounds and {} ms", new Object[] { allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart() }); - - return allAdded; - } - private void processSort(final SearchParameterMap theParams) { // Set loadPids = theLoadPids; @@ -1957,10 +1751,214 @@ public class SearchBuilder { } } + private static List createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From from) { + List lastUpdatedPredicates = new ArrayList(); + if (theLastUpdated != null) { + if (theLastUpdated.getLowerBoundAsInstant() != null) { + Predicate predicateLower = builder.greaterThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()); + lastUpdatedPredicates.add(predicateLower); + } + if (theLastUpdated.getUpperBoundAsInstant() != null) { + Predicate predicateUpper = builder.lessThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()); + lastUpdatedPredicates.add(predicateUpper); + } + } + return lastUpdatedPredicates; + } + private static String createLeftMatchLikeExpression(String likeExpression) { return likeExpression.replace("%", "[%]") + "%"; } + private static List filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection thePids) { + CriteriaBuilder builder = theEntityManager.getCriteriaBuilder(); + CriteriaQuery cq = builder.createQuery(Long.class); + Root from = cq.from(ResourceTable.class); + cq.select(from.get("myId").as(Long.class)); + + List lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from); + lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids)); + + cq.where(SearchBuilder.toArray(lastUpdatedPredicates)); + TypedQuery query = theEntityManager.createQuery(cq); + + List resultList = query.getResultList(); + return resultList; + } + + public static void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, + EntityManager entityManager, FhirContext context, IDao theDao) { + if (theIncludePids.isEmpty()) { + return; + } + + Map position = new HashMap(); + for (Long next : theIncludePids) { + position.put(next, theResourceListToPopulate.size()); + theResourceListToPopulate.add(null); + } + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = builder.createQuery(ResourceTable.class); + Root from = cq.from(ResourceTable.class); + cq.where(from.get("myId").in(theIncludePids)); + TypedQuery q = entityManager.createQuery(cq); + + for (ResourceTable next : q.getResultList()) { + Class resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); + IBaseResource resource = (IBaseResource) theDao.toResource(resourceType, next, theForHistoryOperation); + Integer index = position.get(next.getId()); + if (index == null) { + ourLog.warn("Got back unexpected resource PID {}", next.getId()); + continue; + } + + if (resource instanceof IResource) { + if (theRevIncludedPids.contains(next.getId())) { + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE); + } else { + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH); + } + } else { + if (theRevIncludedPids.contains(next.getId())) { + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode()); + } else { + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode()); + } + } + + theResourceListToPopulate.set(index, resource); + } + } + + /** + * THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet()) + * + * @param theLastUpdated + */ + public static HashSet loadReverseIncludes(FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, + DateRangeParam theLastUpdated) { + if (theMatches.size() == 0) { + return new HashSet(); + } + if (theRevIncludes == null || theRevIncludes.isEmpty()) { + return new HashSet(); + } + String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; + + Collection nextRoundMatches = theMatches; + HashSet allAdded = new HashSet(); + HashSet original = new HashSet(theMatches); + ArrayList includes = new ArrayList(theRevIncludes); + + int roundCounts = 0; + StopWatch w = new StopWatch(); + + boolean addedSomeThisRound; + do { + roundCounts++; + + HashSet pidsToInclude = new HashSet(); + Set nextRoundOmit = new HashSet(); + + for (Iterator iter = includes.iterator(); iter.hasNext();) { + Include nextInclude = iter.next(); + if (nextInclude.isRecurse() == false) { + iter.remove(); + } + + boolean matchAll = "*".equals(nextInclude.getValue()); + if (matchAll) { + String sql; + sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids)"; + TypedQuery q = theEntityManager.createQuery(sql, ResourceLink.class); + q.setParameter("target_pids", nextRoundMatches); + List results = q.getResultList(); + for (ResourceLink resourceLink : results) { + if (theReverseMode) { + // if (theEverythingModeEnum.isEncounter()) { + // if (resourceLink.getSourcePath().equals("Encounter.subject") || + // resourceLink.getSourcePath().equals("Encounter.patient")) { + // nextRoundOmit.add(resourceLink.getSourceResourcePid()); + // } + // } + pidsToInclude.add(resourceLink.getSourceResourcePid()); + } else { + pidsToInclude.add(resourceLink.getTargetResourcePid()); + } + } + } else { + + List paths; + if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { + paths = Collections.singletonList(nextInclude.getValue()); + } else { + String resType = nextInclude.getParamType(); + if (isBlank(resType)) { + continue; + } + RuntimeResourceDefinition def = theContext.getResourceDefinition(resType); + if (def == null) { + ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); + continue; + } + + String paramName = nextInclude.getParamName(); + RuntimeSearchParam param = isNotBlank(paramName) ? def.getSearchParam(paramName) : null; + if (param == null) { + ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); + continue; + } + + paths = param.getPathsSplit(); + } + + String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); + for (String nextPath : paths) { + String sql; + if (targetResourceType != null) { + sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type"; + } else { + sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; + } + TypedQuery q = theEntityManager.createQuery(sql, ResourceLink.class); + q.setParameter("src_path", nextPath); + q.setParameter("target_pids", nextRoundMatches); + if (targetResourceType != null) { + q.setParameter("target_resource_type", targetResourceType); + } + List results = q.getResultList(); + for (ResourceLink resourceLink : results) { + if (theReverseMode) { + pidsToInclude.add(resourceLink.getSourceResourcePid()); + } else { + pidsToInclude.add(resourceLink.getTargetResourcePid()); + } + } + } + } + } + + if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) { + pidsToInclude = new HashSet(filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude)); + } + for (Long next : pidsToInclude) { + if (original.contains(next) == false && allAdded.contains(next) == false) { + theMatches.add(next); + } + } + + pidsToInclude.removeAll(nextRoundOmit); + + addedSomeThisRound = allAdded.addAll(pidsToInclude); + nextRoundMatches = pidsToInclude; + } while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound); + + ourLog.info("Loaded {} {} in {} rounds and {} ms", new Object[] { allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart() }); + + return allAdded; + } + static Predicate[] toArray(List thePredicates) { return thePredicates.toArray(new Predicate[thePredicates.size()]); } @@ -2021,117 +2019,4 @@ public class SearchBuilder { } } - public final static class BundleProviderPersisted implements IBundleProvider { - - private String myUuid; - private PlatformTransactionManager myPlatformTransactionManager; - private ISearchResultDao mySearchResultDao; - private Search mySearchEntity; - private EntityManager myEntityManager; - private FhirContext myContext; - private IDao myDao; - - public BundleProviderPersisted(String theSearchUuid, PlatformTransactionManager thePlatformTransactionManager, ISearchResultDao theSearchResultDao, EntityManager theEntityManager, - FhirContext theContext, IDao theDao) { - myUuid = theSearchUuid; - myPlatformTransactionManager = thePlatformTransactionManager; - mySearchResultDao = theSearchResultDao; - myEntityManager = theEntityManager; - myContext = theContext; - myDao = theDao; - } - - @Override - public InstantDt getPublished() { - ensureSearchEntityLoaded(); - return new InstantDt(mySearchEntity.getCreated()); - } - - @Override - public List getResources(final int theFromIndex, final int theToIndex) { - TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); - - return template.execute(new TransactionCallback>() { - @Override - public List doInTransaction(TransactionStatus theStatus) { - ensureSearchEntityLoaded(); - - int pageSize = theToIndex - theFromIndex; - if (pageSize < 1) { - return Collections.emptyList(); - } - - int pageIndex = theFromIndex / pageSize; - - Pageable page = new PageRequest(pageIndex, pageSize); - Page search = mySearchResultDao.findWithSearchUuid(mySearchEntity, page); - - List pidsSubList = new ArrayList(); - for (SearchResult next : search) { - pidsSubList.add(next.getResourcePid()); - } - - // Load includes - pidsSubList = new ArrayList(pidsSubList); - - Set revIncludedPids = new HashSet(); - if (mySearchEntity.getEverythingMode() == null) { - revIncludedPids.addAll(loadReverseIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated())); - } - revIncludedPids.addAll(loadReverseIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated())); - - // Execute the query and make sure we return distinct results - List resources = new ArrayList(); - loadResourcesByPid(pidsSubList, resources, revIncludedPids, false, myEntityManager, myContext, myDao); - - return resources; - } - - }); - } - - @Override - public Integer preferredPageSize() { - ensureSearchEntityLoaded(); - return mySearchEntity.getPreferredPageSize(); - } - - @Override - public int size() { - ensureSearchEntityLoaded(); - return mySearchEntity.getTotalCount(); - } - - public String getSearchUuid() { - return myUuid; - } - - /** - * Returns false if the entity can't be found - */ - public boolean ensureSearchEntityLoaded() { - if (mySearchEntity == null) { - TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); - template.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); - return template.execute(new TransactionCallback() { - @Override - public Boolean doInTransaction(TransactionStatus theStatus) { - TypedQuery q = myEntityManager.createQuery("SELECT s FROM Search s WHERE s.myUuid = :uuid", Search.class); - q.setParameter("uuid", myUuid); - try { - mySearchEntity = q.getSingleResult(); - - // Ensure includes are loaded - mySearchEntity.getIncludes().size(); - return true; - } catch (NoResultException e) { - return false; - } - } - }); - } - return true; - } - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java new file mode 100644 index 00000000000..990ab9af93c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java @@ -0,0 +1,106 @@ +package ca.uhn.fhir.jpa.dao.data; + +import java.util.Date; +import java.util.List; + +import javax.persistence.TemporalType; + +import org.springframework.data.domain.Pageable; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2016 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.Temporal; +import org.springframework.data.repository.query.Param; + +import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; + +public interface IResourceHistoryTableDao extends JpaRepository { + //@formatter:off + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myUpdated >= :cutoff") + int countForAllResourceTypes( + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff + ); + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceId = :id AND t.myUpdated >= :cutoff") + int countForResourceInstance( + @Param("id") Long theId, + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff + ); + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceType = :type AND t.myUpdated >= :cutoff") + int countForResourceType( + @Param("type") String theType, + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff + ); + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t") + int countForAllResourceTypes( + ); + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceId = :id") + int countForResourceInstance( + @Param("id") Long theId + ); + + @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceType = :type") + int countForResourceType( + @Param("type") String theType + ); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myUpdated >= :cutoff ORDER BY t.myUpdated DESC") + List findForAllResourceTypes( + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff, + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myResourceId = :id AND t.myUpdated >= :cutoff ORDER BY t.myUpdated DESC") + List findForResourceInstance( + @Param("id") Long theId, + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff, + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myResourceType = :type AND t.myUpdated >= :cutoff ORDER BY t.myUpdated DESC") + List findForResourceType( + @Param("type") String theType, + @Temporal(value=TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff, + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t ORDER BY t.myUpdated DESC") + List findForAllResourceTypes( + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myResourceId = :id ORDER BY t.myUpdated DESC") + List findForResourceInstance( + @Param("id") Long theId, + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myResourceType = :type ORDER BY t.myUpdated DESC") + List findForResourceType( + @Param("type") String theType, + Pageable thePageable); + + @Query("SELECT t FROM ResourceHistoryTable t WHERE t.myResourceId = :id AND t.myResourceVersion = :version") + ResourceHistoryTable findForIdAndVersion(@Param("id") long theId, @Param("version") long theVersion); + + //@formatter:on +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java index c2b261ffaf2..8b9a8c67e88 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java @@ -25,5 +25,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import ca.uhn.fhir.jpa.entity.ResourceTable; public interface IResourceTableDao extends JpaRepository { - + // nothing yet } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java index f2e0e13be9d..7561cdd27a1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java @@ -29,9 +29,12 @@ import org.springframework.data.repository.query.Param; import ca.uhn.fhir.jpa.entity.Search; -public interface ISearchDao extends JpaRepository { +public interface ISearchDao extends JpaRepository { + + @Query("SELECT s FROM Search s WHERE s.myUuid = :uuid") + public Search findByUuid(@Param("uuid") String theUuid); @Query("SELECT s FROM Search s WHERE s.myCreated < :cutoff") public Collection findWhereCreatedBefore(@Param("cutoff") Date theCutoff); - + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java index db880eff864..1f4380e7a65 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java @@ -31,21 +31,25 @@ import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.JoinColumn; +import javax.persistence.Index; import javax.persistence.OneToMany; -import javax.persistence.OneToOne; import javax.persistence.SequenceGenerator; import javax.persistence.Table; import javax.persistence.UniqueConstraint; -import org.hibernate.annotations.Index; - import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.server.Constants; +//@formatter:off @Entity -@Table(name = "HFJ_RES_VER", uniqueConstraints = { @UniqueConstraint(name = "IDX_RES_VER_ALL", columnNames = { "RES_ID", "RES_TYPE", "RES_VER" }) }) -@org.hibernate.annotations.Table(appliesTo = "HFJ_RES_VER", indexes = { @Index(name = "IDX_RES_VER_DATE", columnNames = { "RES_UPDATED" }) }) +@Table(name = "HFJ_RES_VER", uniqueConstraints = { + @UniqueConstraint(name="IDX_RESVER_ID_VER", columnNames = { "RES_ID", "RES_VER" }) +}, indexes= { + @Index(name="IDX_RESVER_TYPE_DATE", columnList="RES_TYPE,RES_UPDATED"), + @Index(name="IDX_RESVER_ID_DATE", columnList="RES_ID,RES_UPDATED"), + @Index(name="IDX_RESVER_DATE", columnList="RES_UPDATED") +}) +//@formatter:on public class ResourceHistoryTable extends BaseHasResource implements Serializable { private static final long serialVersionUID = 1L; @@ -68,13 +72,6 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl @OneToMany(mappedBy = "myResourceHistory", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) private Collection myTags; - /** - * This field is only populated if this specific histor entry corresponds to - * the most recent version of a resource - */ - @OneToOne(fetch=FetchType.LAZY, optional=true, mappedBy="myHistory") - private ResourceTable myCorrespondsToVersion; - public void addTag(ResourceHistoryTag theTag) { for (ResourceHistoryTag next : getTags()) { if (next.getTag().equals(theTag)) { @@ -89,7 +86,7 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl tag.setResourceType(theTag.getResourceType()); getTags().add(tag); } - + @Override public BaseTag addTag(TagDefinition theDef) { ResourceHistoryTag historyTag = new ResourceHistoryTag(this, theDef); @@ -139,6 +136,10 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl return false; } + public void setId(Long theId) { + myId = theId; + } + public void setResourceId(Long theResourceId) { myResourceId = theResourceId; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index 75f7ab4cd97..89fc12e9f3e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -35,9 +35,7 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; -import javax.persistence.JoinColumn; import javax.persistence.OneToMany; -import javax.persistence.OneToOne; import javax.persistence.Table; import javax.persistence.Transient; @@ -152,10 +150,6 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Column(name = "SP_HAS_LINKS") private boolean myHasLinks; - @OneToOne(fetch=FetchType.LAZY, optional=true) - @JoinColumn(name="HISTORY_VERSION_PID", referencedColumnName="PID", nullable=true) - private ResourceHistoryTable myHistory; - @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "RES_ID") @@ -191,10 +185,10 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Column(name = "SP_DATE_PRESENT") private boolean myParamsDatePopulated; - + @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) private Collection myParamsNumber; - + @Column(name = "SP_NUMBER_PRESENT") private boolean myParamsNumberPopulated; @@ -534,8 +528,8 @@ public class ResourceTable extends BaseHasResource implements Serializable { myVersion = theVersion; } - public ResourceHistoryTable toHistory() { - ResourceHistoryTable retVal = new ResourceHistoryTable(); + public ResourceHistoryTable toHistory(ResourceHistoryTable theResourceHistoryTable) { + ResourceHistoryTable retVal = theResourceHistoryTable != null ? theResourceHistoryTable : new ResourceHistoryTable(); retVal.setResourceId(myId); retVal.setResourceType(myResourceType); @@ -550,6 +544,9 @@ public class ResourceTable extends BaseHasResource implements Serializable { retVal.setDeleted(getDeleted()); retVal.setForcedId(getForcedId()); + retVal.getTags().clear(); + + retVal.setHasTags(isHasTags()); if (isHasTags()) { for (ResourceTag next : getTags()) { retVal.addTag(next); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java index 00b193042b0..652d340e12d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java @@ -43,7 +43,6 @@ import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.UniqueConstraint; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -63,16 +62,12 @@ public class Search implements Serializable { @Column(name="CREATED", nullable=false, updatable=false) private Date myCreated; - @Enumerated(EnumType.ORDINAL) - @Column(name="EVERYTHING_MODE", nullable=true) - private EverythingModeEnum myEverythingMode; - @Id @GeneratedValue(strategy = GenerationType.AUTO, generator="SEQ_SEARCH") @SequenceGenerator(name="SEQ_SEARCH", sequenceName="SEQ_SEARCH") @Column(name = "PID") private Long myId; - + @OneToMany(mappedBy="mySearch") private Collection myIncludes; @@ -87,23 +82,29 @@ public class Search implements Serializable { @Column(name="PREFERRED_PAGE_SIZE", nullable=true) private Integer myPreferredPageSize; + @Column(name="RESOURCE_ID", nullable=true) + private Long myResourceId; + + @Column(name="RESOURCE_TYPE", length=200, nullable=true) + private String myResourceType; + @OneToMany(mappedBy="mySearch") private Collection myResults; + @Enumerated(EnumType.ORDINAL) + @Column(name="SEARCH_TYPE", nullable=false) + private SearchTypeEnum mySearchType; + @Column(name="TOTAL_COUNT") private int myTotalCount; @Column(name="SEARCH_UUID", length=40, nullable=false, updatable=false) private String myUuid; - + public Date getCreated() { return myCreated; } - public EverythingModeEnum getEverythingMode() { - return myEverythingMode; - } - public Long getId() { return myId; } @@ -114,6 +115,10 @@ public class Search implements Serializable { } return myIncludes; } + + public Date getLastUpdatedHigh() { + return myLastUpdatedHigh; + } public DateRangeParam getLastUpdated() { if (myLastUpdatedLow == null && myLastUpdatedHigh == null) { @@ -127,6 +132,19 @@ public class Search implements Serializable { return myPreferredPageSize; } + public Long getResourceId() { + return myResourceId; + } + + public String getResourceType() { + return myResourceType; + } + + public SearchTypeEnum getSearchType() { + return mySearchType; + } + + public int getTotalCount() { return myTotalCount; } @@ -134,15 +152,15 @@ public class Search implements Serializable { public String getUuid() { return myUuid; } - + public void setCreated(Date theCreated) { myCreated = theCreated; } - - public void setEverythingMode(EverythingModeEnum theEverythingMode) { - myEverythingMode = theEverythingMode; + public void setLastUpdated(Date theLowerBound, Date theUpperBound) { + myLastUpdatedLow = theLowerBound; + myLastUpdatedHigh = theUpperBound; } - + public void setLastUpdated(DateRangeParam theLastUpdated) { if (theLastUpdated == null) { myLastUpdatedLow = null; @@ -156,6 +174,19 @@ public class Search implements Serializable { public void setPreferredPageSize(Integer thePreferredPageSize) { myPreferredPageSize = thePreferredPageSize; } + + public void setResourceId(Long theResourceId) { + myResourceId = theResourceId; + } + + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public void setSearchType(SearchTypeEnum theSearchType) { + mySearchType = theSearchType; + } public void setTotalCount(int theTotalCount) { myTotalCount = theTotalCount; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchTypeEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchTypeEnum.java new file mode 100644 index 00000000000..4a0057942c8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchTypeEnum.java @@ -0,0 +1,9 @@ +package ca.uhn.fhir.jpa.entity; + +public enum SearchTypeEnum { + + EVERYTHING, + SEARCH, + HISTORY, + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java index 8d3e1dd834c..48c93fb9a1a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java @@ -37,6 +37,8 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import ca.uhn.fhir.model.api.Tag; @@ -145,5 +147,17 @@ public class TagDefinition implements Serializable { b.append(myId); return b.toHashCode(); } + + @Override + public String toString() { + ToStringBuilder retVal = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + retVal.append("id", myId); + retVal.append("system", mySystem); + retVal.append("code", myCode); + retVal.append("display", myDisplay); + return retVal.build(); + } + + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java index 1a48dfe21af..2410fd77f4a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java @@ -9,8 +9,6 @@ import org.springframework.transaction.PlatformTransactionManager; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.SearchBuilder; -import ca.uhn.fhir.jpa.dao.SearchBuilder.BundleProviderPersisted; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IBundleProvider; @@ -36,8 +34,7 @@ public class DatabaseBackedPagingProvider extends FifoMemoryPagingProvider { public synchronized IBundleProvider retrieveResultList(String theId) { IBundleProvider retVal = super.retrieveResultList(theId); if (retVal == null) { - BundleProviderPersisted provider = new SearchBuilder.BundleProviderPersisted(theId, thePlatformTransactionManager, theSearchResultDao, theEntityManager, - theContext, theDao); + PersistedJpaBundleProvider provider = new PersistedJpaBundleProvider(theId, theDao); if (!provider.ensureSearchEntityLoaded()) { return null; } @@ -48,8 +45,8 @@ public class DatabaseBackedPagingProvider extends FifoMemoryPagingProvider { @Override public synchronized String storeResultList(IBundleProvider theList) { - if (theList instanceof SearchBuilder.BundleProviderPersisted) { - return ((BundleProviderPersisted)theList).getSearchUuid(); + if (theList instanceof PersistedJpaBundleProvider) { + return ((PersistedJpaBundleProvider)theList).getSearchUuid(); } return super.storeResultList(theList); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java new file mode 100644 index 00000000000..ead7cc64de2 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java @@ -0,0 +1,246 @@ +package ca.uhn.fhir.jpa.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.IDao; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.dao.data.ISearchDao; +import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; +import ca.uhn.fhir.jpa.entity.BaseHasResource; +import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.entity.SearchResult; +import ca.uhn.fhir.jpa.entity.SearchTypeEnum; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.server.IBundleProvider; + +public final class PersistedJpaBundleProvider implements IBundleProvider { + + private FhirContext myContext; + private IDao myDao; + private EntityManager myEntityManager; + private PlatformTransactionManager myPlatformTransactionManager; + private IResourceHistoryTableDao myResourceHistoryTableDao; + private ISearchDao mySearchDao; + private Search mySearchEntity; + private ISearchResultDao mySearchResultDao; + private String myUuid; + + public PersistedJpaBundleProvider(String theSearchUuid, IDao theDao) { + myUuid = theSearchUuid; + myDao = theDao; + } + + @Autowired + private IResourceTableDao myResourceTableDao; + + protected List doHistoryInTransaction(int theFromIndex, int theToIndex) { + + Date cutoff = mySearchEntity.getLastUpdatedHigh(); + + Pageable pageable = toPage(theFromIndex, theToIndex); + + List results; + + if (cutoff != null) { + if (mySearchEntity.getResourceType() == null) { + results = myResourceHistoryTableDao.findForAllResourceTypes(cutoff, pageable); + } else if (mySearchEntity.getResourceId() == null) { + results = myResourceHistoryTableDao.findForResourceType(mySearchEntity.getResourceType(), cutoff, pageable); + } else { + results = myResourceHistoryTableDao.findForResourceInstance(mySearchEntity.getResourceId(), cutoff, pageable); + } + } else { + if (mySearchEntity.getResourceType() == null) { + results = myResourceHistoryTableDao.findForAllResourceTypes(pageable); + } else if (mySearchEntity.getResourceId() == null) { + results = myResourceHistoryTableDao.findForResourceType(mySearchEntity.getResourceType(), pageable); + } else { + results = myResourceHistoryTableDao.findForResourceInstance(mySearchEntity.getResourceId(), pageable); + } + } + + ArrayList retVal = new ArrayList(); + for (ResourceHistoryTable next : results) { + BaseHasResource resource; + resource = next; + + retVal.add(myDao.toResource(resource, true)); + } + + return retVal; + } + + protected List doSearchOrEverythingInTransaction(final int theFromIndex, final int theToIndex) { + + Pageable page = toPage(theFromIndex, theToIndex); + if (page == null) { + return Collections.emptyList(); + } + + Page search = mySearchResultDao.findWithSearchUuid(mySearchEntity, page); + + List pidsSubList = new ArrayList(); + for (SearchResult next : search) { + pidsSubList.add(next.getResourcePid()); + } + + // Load includes + pidsSubList = new ArrayList(pidsSubList); + + Set revIncludedPids = new HashSet(); + if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) { + revIncludedPids.addAll(SearchBuilder.loadReverseIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated())); + } + revIncludedPids.addAll(SearchBuilder.loadReverseIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated())); + + // Execute the query and make sure we return distinct results + List resources = new ArrayList(); + SearchBuilder.loadResourcesByPid(pidsSubList, resources, revIncludedPids, false, myEntityManager, myContext, myDao); + + return resources; + } + + /** + * Returns false if the entity can't be found + */ + public boolean ensureSearchEntityLoaded() { + if (mySearchEntity == null) { + ensureDependenciesInjected(); + + TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); + template.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); + return template.execute(new TransactionCallback() { + @Override + public Boolean doInTransaction(TransactionStatus theStatus) { + try { + mySearchEntity = mySearchDao.findByUuid(myUuid); + + if (mySearchEntity == null) { + return false; + } + + // Load the includes now so that they are available outside of this transaction + mySearchEntity.getIncludes().size(); + + return true; + } catch (NoResultException e) { + return false; + } + } + }); + } + return true; + } + + private void ensureDependenciesInjected() { + if (myPlatformTransactionManager == null) { + myDao.injectDependenciesIntoBundleProvider(this); + } + } + + @Override + public InstantDt getPublished() { + ensureSearchEntityLoaded(); + return new InstantDt(mySearchEntity.getCreated()); + } + + @Override + public List getResources(final int theFromIndex, final int theToIndex) { + ensureDependenciesInjected(); + + TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); + + return template.execute(new TransactionCallback>() { + @Override + public List doInTransaction(TransactionStatus theStatus) { + ensureSearchEntityLoaded(); + + switch (mySearchEntity.getSearchType()) { + case HISTORY: + return doHistoryInTransaction(theFromIndex, theToIndex); + case SEARCH: + case EVERYTHING: + default: + return doSearchOrEverythingInTransaction(theFromIndex, theToIndex); + } + } + + + }); + } + + public String getSearchUuid() { + return myUuid; + } + + @Override + public Integer preferredPageSize() { + ensureSearchEntityLoaded(); + return mySearchEntity.getPreferredPageSize(); + } + + public void setContext(FhirContext theContext) { + myContext = theContext; + } + + public void setEntityManager(EntityManager theEntityManager) { + myEntityManager = theEntityManager; + } + + + public void setPlatformTransactionManager(PlatformTransactionManager thePlatformTransactionManager) { + myPlatformTransactionManager = thePlatformTransactionManager; + } + + public void setResourceHistoryTableDao(IResourceHistoryTableDao theResourceHistoryTableDao) { + myResourceHistoryTableDao = theResourceHistoryTableDao; + } + + public void setSearchDao(ISearchDao theSearchDao) { + mySearchDao = theSearchDao; + } + + public void setSearchResultDao(ISearchResultDao theSearchResultDao) { + mySearchResultDao = theSearchResultDao; + } + + @Override + public int size() { + ensureSearchEntityLoaded(); + return mySearchEntity.getTotalCount(); + } + + private Pageable toPage(int theFromIndex, int theToIndex) { + int pageSize = theToIndex - theFromIndex; + if (pageSize < 1) { + return null; + } + + int pageIndex = theFromIndex / pageSize; + + Pageable page = new PageRequest(pageIndex, pageSize); + return page; + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu1Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu1Config.java index 642390c4244..6af61ccbf24 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu1Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu1Config.java @@ -55,7 +55,7 @@ public class TestDstu1Config extends BaseJavaConfigDstu1 { private Properties jpaProperties() { Properties extraProperties = new Properties(); - extraProperties.put("hibernate.format_sql", "true"); + extraProperties.put("hibernate.format_sql", "false"); extraProperties.put("hibernate.show_sql", "false"); extraProperties.put("hibernate.hbm2ddl.auto", "update"); return extraProperties; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1Test.java index daabb0ed5d2..525f8082cd2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1Test.java @@ -19,6 +19,8 @@ import java.util.Date; import java.util.List; import java.util.Map; +import javax.persistence.EntityManager; + import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; @@ -26,6 +28,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.transaction.PlatformTransactionManager; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.TestDstu1Config; @@ -59,6 +62,14 @@ public class FhirSystemDaoDstu1Test extends BaseJpaTest { private static IFhirResourceDao ourPatientDao; private static IFhirSystemDao, MetaDt> ourSystemDao; private RequestDetails myRequestDetails; + private static EntityManager ourEntityManager; + private static PlatformTransactionManager ourTxManager; + + @Before + public void before() { + myRequestDetails = mock(RequestDetails.class); + super.purgeDatabase(ourEntityManager, ourTxManager); + } @Test public void testGetResourceCounts() { @@ -133,7 +144,7 @@ public class FhirSystemDaoDstu1Test extends BaseJpaTest { assertEquals(1, values.size()); } - + @Test public void testPersistWithSimpleLink() { Patient patient = new Patient(); @@ -187,11 +198,6 @@ public class FhirSystemDaoDstu1Test extends BaseJpaTest { assertEquals(obsVersion2, "2"); } - - @Before - public void before() { - myRequestDetails = mock(RequestDetails.class); - } @Test public void testPersistWithUnknownId() { @@ -484,6 +490,8 @@ public class FhirSystemDaoDstu1Test extends BaseJpaTest { ourObservationDao = ourCtx.getBean("myObservationDaoDstu1", IFhirResourceDao.class); ourLocationDao = ourCtx.getBean("myLocationDaoDstu1", IFhirResourceDao.class); ourSystemDao = ourCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); + ourEntityManager = ourCtx.getBean(EntityManager.class); + ourTxManager = ourCtx.getBean(PlatformTransactionManager.class); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java index 8101cd676eb..3c4909dd676 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java @@ -112,6 +112,28 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2Test.class); + private void assertGone(IIdType theId) { + try { + assertNotGone(theId); + fail(); + } catch (ResourceGoneException e) { + // good + } + } + + /** + * This gets called from assertGone too! Careful about exceptions... + */ + private void assertNotGone(IIdType theId) { + if ("Patient".equals(theId.getResourceType())) { + myPatientDao.read(theId, new ServletRequestDetails()); + } else if ("Organization".equals(theId.getResourceType())){ + myOrganizationDao.read(theId, new ServletRequestDetails()); + } else { + fail("No type"); + } + } + private List extractNames(IBundleProvider theSearch) { ArrayList retVal = new ArrayList(); for (IBaseResource next : theSearch.getResources(0, theSearch.size())) { @@ -121,6 +143,14 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { return retVal; } + private String log(IBundleProvider theHistory) { + StringBuilder b =new StringBuilder(theHistory.size() + " results: "); + for (IBaseResource next : theHistory.getResources(0, theHistory.size())) { + b.append("\n ").append(next.getIdElement().toUnqualified().getValue()); + } + return b.toString(); + } + private void sort(TagList thePublished) { ArrayList tags = new ArrayList(thePublished); Collections.sort(tags, new Comparator() { @@ -743,7 +773,8 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertThat(found, empty()); } - + + @Test public void testDeleteResource() { int initialHistory = myPatientDao.history(null, new ServletRequestDetails()).size(); @@ -806,6 +837,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertEquals(0, patients.size()); } + @Test public void testDeleteThenUndelete() { @@ -840,27 +872,44 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertEquals(id2, gotId); } + @Test - public void testDeleteWithMatchUrlChainedString() { - String methodName = "testDeleteWithMatchUrlChainedString"; - - Organization org = new Organization(); - org.setName(methodName); - IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + public void testDeleteWithMatchUrl() { + String methodName = "testDeleteWithMatchUrl"; Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue(methodName); - p.getManagingOrganization().setReference(orgId); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId(); ourLog.info("Created patient, got it: {}", id); - myPatientDao.deleteByUrl("Patient?organization.name=" + methodName, new ServletRequestDetails()); + Bundle request = new Bundle(); + request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName); + + myPatientDao.deleteByUrl("Patient?identifier=urn%3Asystem%7C" + methodName, new ServletRequestDetails()); + + try { + myPatientDao.read(id.toVersionless(), new ServletRequestDetails()); + fail(); + } catch (ResourceGoneException e) { + // ok + } + + try { + myPatientDao.read(new IdDt("Patient/" + methodName), new ServletRequestDetails()); + fail(); + } catch (ResourceNotFoundException e) { + // ok + } + + IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); + assertEquals(2, history.size()); + + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 1).get(0)).getValue()); + assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(1, 2).get(0))); - assertGone(id); } - - + @Test public void testDeleteWithMatchUrlChainedIdentifier() { String methodName = "testDeleteWithMatchUrlChainedIdentifer"; @@ -886,7 +935,74 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertGone(orgId); } + @Test + public void testDeleteWithMatchUrlChainedProfile() { + String methodName = "testDeleteWithMatchUrlChainedProfile"; + + List profileList = new ArrayList(); + profileList.add(new IdDt("http://foo")); + + Organization org = new Organization(); + ResourceMetadataKeyEnum.PROFILES.put(org, profileList); + org.setName(methodName); + + IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.getManagingOrganization().setReference(orgId); + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + ourLog.info("Created patient, got it: {}", id); + + myPatientDao.deleteByUrl("Patient?organization._profile=http://foo", new ServletRequestDetails()); + assertGone(id); + + myOrganizationDao.deleteByUrl("Organization?_profile=http://foo", new ServletRequestDetails()); + try { + myOrganizationDao.read(orgId, new ServletRequestDetails()); + fail(); + } catch (ResourceGoneException e) { + // good + } + + try { + myPatientDao.deleteByUrl("Patient?organization._profile.identifier=http://foo", new ServletRequestDetails()); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid parameter chain: organization._profile.identifier", e.getMessage()); + } + + try { + myOrganizationDao.deleteByUrl("Organization?_profile.identifier=http://foo", new ServletRequestDetails()); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid parameter chain: _profile.identifier", e.getMessage()); + } + + } + + + @Test + public void testDeleteWithMatchUrlChainedString() { + String methodName = "testDeleteWithMatchUrlChainedString"; + + Organization org = new Organization(); + org.setName(methodName); + IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.getManagingOrganization().setReference(orgId); + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + ourLog.info("Created patient, got it: {}", id); + + myPatientDao.deleteByUrl("Patient?organization.name=" + methodName, new ServletRequestDetails()); + + assertGone(id); + } @Test public void testDeleteWithMatchUrlChainedTag() { @@ -935,7 +1051,6 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { } - @Test public void testDeleteWithMatchUrlQualifierMissing() { String methodName = "testDeleteWithMatchUrlChainedProfile"; @@ -997,113 +1112,6 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertGone(org2Id); } - /** - * This gets called from assertGone too! Careful about exceptions... - */ - private void assertNotGone(IIdType theId) { - if ("Patient".equals(theId.getResourceType())) { - myPatientDao.read(theId, new ServletRequestDetails()); - } else if ("Organization".equals(theId.getResourceType())){ - myOrganizationDao.read(theId, new ServletRequestDetails()); - } else { - fail("No type"); - } - } - private void assertGone(IIdType theId) { - try { - assertNotGone(theId); - fail(); - } catch (ResourceGoneException e) { - // good - } - } - - - - @Test - public void testDeleteWithMatchUrlChainedProfile() { - String methodName = "testDeleteWithMatchUrlChainedProfile"; - - List profileList = new ArrayList(); - profileList.add(new IdDt("http://foo")); - - Organization org = new Organization(); - ResourceMetadataKeyEnum.PROFILES.put(org, profileList); - org.setName(methodName); - - IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - p.getManagingOrganization().setReference(orgId); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - ourLog.info("Created patient, got it: {}", id); - - myPatientDao.deleteByUrl("Patient?organization._profile=http://foo", new ServletRequestDetails()); - assertGone(id); - - myOrganizationDao.deleteByUrl("Organization?_profile=http://foo", new ServletRequestDetails()); - try { - myOrganizationDao.read(orgId, new ServletRequestDetails()); - fail(); - } catch (ResourceGoneException e) { - // good - } - - try { - myPatientDao.deleteByUrl("Patient?organization._profile.identifier=http://foo", new ServletRequestDetails()); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Invalid parameter chain: organization._profile.identifier", e.getMessage()); - } - - try { - myOrganizationDao.deleteByUrl("Organization?_profile.identifier=http://foo", new ServletRequestDetails()); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Invalid parameter chain: _profile.identifier", e.getMessage()); - } - - } - - @Test - public void testDeleteWithMatchUrl() { - String methodName = "testDeleteWithMatchUrl"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId(); - ourLog.info("Created patient, got it: {}", id); - - Bundle request = new Bundle(); - request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName); - - myPatientDao.deleteByUrl("Patient?identifier=urn%3Asystem%7C" + methodName, new ServletRequestDetails()); - - try { - myPatientDao.read(id.toVersionless(), new ServletRequestDetails()); - fail(); - } catch (ResourceGoneException e) { - // ok - } - - try { - myPatientDao.read(new IdDt("Patient/" + methodName), new ServletRequestDetails()); - fail(); - } catch (ResourceNotFoundException e) { - // ok - } - - IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); - assertEquals(2, history.size()); - - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 0).get(0))); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 0).get(0)).getValue()); - assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(1, 1).get(0))); - - } - @Test public void testHistoryByForcedId() { IIdType idv1; @@ -1287,14 +1295,6 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { } - private String log(IBundleProvider theHistory) { - StringBuilder b =new StringBuilder(theHistory.size() + " results: "); - for (IBaseResource next : theHistory.getResources(0, theHistory.size())) { - b.append("\n ").append(next.getIdElement().toUnqualified().getValue()); - } - return b.toString(); - } - @Test public void testHistoryWithDeletedResource() throws Exception { String methodName = "testHistoryWithDeletedResource"; @@ -2263,79 +2263,6 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { } - @Test - public void testSortByLastUpdated() { - String methodName = "testSortByLastUpdated"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system1").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system2").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id2 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system3").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id3 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system4").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id4 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - SearchParameterMap pm; - List actual; - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id1, id2, id3, id4)); - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id1, id2, id3, id4)); - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id4, id3, id2, id1)); - - pm = new SearchParameterMap(); - pm.add(Patient.SP_IDENTIFIER, new TokenParam(null, methodName)); - pm.setSort(new SortSpec(Patient.SP_NAME).setChain(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC))); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id4, id3, id2, id1)); - } - - @Test - public void testSortNoMatches() { - String methodName = "testSortNoMatches"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - SearchParameterMap map; - - map = new SearchParameterMap(); - map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); - map.setLastUpdated(new DateRangeParam("2001", "2003")); - map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); - assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); - - map = new SearchParameterMap(); - map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); - map.setLastUpdated(new DateRangeParam("2001", "2003")); - map.setSort(new SortSpec(Patient.SP_NAME)); - assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); - - } - @Test public void testSortById() { String methodName = "testSortBTyId"; @@ -2387,6 +2314,55 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertThat(actual, contains(id4, id3, id2, id1, idMethodName)); } + @Test + public void testSortByLastUpdated() { + String methodName = "testSortByLastUpdated"; + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system1").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system2").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id2 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system3").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id3 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system4").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id4 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + SearchParameterMap pm; + List actual; + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id4, id3, id2, id1)); + + pm = new SearchParameterMap(); + pm.add(Patient.SP_IDENTIFIER, new TokenParam(null, methodName)); + pm.setSort(new SortSpec(Patient.SP_NAME).setChain(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC))); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id4, id3, id2, id1)); + } + @Test public void testSortByNumber() { String methodName = "testSortByNumber"; @@ -2714,6 +2690,30 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { } + @Test + public void testSortNoMatches() { + String methodName = "testSortNoMatches"; + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); + map.setLastUpdated(new DateRangeParam("2001", "2003")); + map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); + + map = new SearchParameterMap(); + map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); + map.setLastUpdated(new DateRangeParam("2001", "2003")); + map.setSort(new SortSpec(Patient.SP_NAME)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); + + } + @Test public void testStoreUnversionedResources() { Organization o1 = new Organization(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java index 08216799925..51e57449688 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java @@ -745,9 +745,9 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest { IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); assertEquals(2, history.size()); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 0).get(0))); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 0).get(0)).getValue()); - assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(1, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(0, 1).get(0)).getValue()); + assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) history.getResources(1, 2).get(0))); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index face966346f..f33d0d8670a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -80,6 +80,7 @@ import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; import ca.uhn.fhir.rest.api.MethodOutcome; @@ -109,6 +110,28 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3Test.class); + private void assertGone(IIdType theId) { + try { + assertNotGone(theId); + fail(); + } catch (ResourceGoneException e) { + // good + } + } + + /** + * This gets called from assertGone too! Careful about exceptions... + */ + private void assertNotGone(IIdType theId) { + if ("Patient".equals(theId.getResourceType())) { + myPatientDao.read(theId, new ServletRequestDetails()); + } else if ("Organization".equals(theId.getResourceType())) { + myOrganizationDao.read(theId, new ServletRequestDetails()); + } else { + fail("No type"); + } + } + private List extractNames(IBundleProvider theSearch) { ArrayList retVal = new ArrayList(); for (IBaseResource next : theSearch.getResources(0, theSearch.size())) { @@ -118,6 +141,12 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { return retVal; } + private CodeableConcept newCodeableConcept(String theSystem, String theCode) { + CodeableConcept retVal = new CodeableConcept(); + retVal.addCoding().setSystem(theSystem).setCode(theCode); + return retVal; + } + private void sort(ArrayList thePublished) { ArrayList tags = new ArrayList(thePublished); Collections.sort(tags, new Comparator() { @@ -202,6 +231,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } + @Test public void testChoiceParamDate() { Observation o2 = new Observation(); @@ -215,7 +245,8 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { assertEquals(id2, found.getResources(0, 1).get(0).getIdElement()); } } - + + @Test public void testChoiceParamDateAlt() { Observation o2 = new Observation(); @@ -272,47 +303,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } - - @Test - public void testChoiceParamQuantityPrecision() { - Observation o3 = new Observation(); - o3.getCode().addCoding().setSystem("foo").setCode("testChoiceParam03"); - o3.setValue(new Quantity(null, 123.01, "foo", "bar", "bar")); - IIdType id3 = myObservationDao.create(o3, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder(id3)); - } - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder(id3)); - } - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.01", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder(id3)); - } - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.010", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder(id3)); - } - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.02", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder()); - } - { - IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.001", "foo", "bar")); - List list = toUnqualifiedVersionlessIds(found); - assertThat(list, containsInAnyOrder()); - } - } - - @Test public void testChoiceParamQuantity() { Observation o3 = new Observation(); @@ -372,6 +362,45 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } + @Test + public void testChoiceParamQuantityPrecision() { + Observation o3 = new Observation(); + o3.getCode().addCoding().setSystem("foo").setCode("testChoiceParam03"); + o3.setValue(new Quantity(null, 123.01, "foo", "bar", "bar")); + IIdType id3 = myObservationDao.create(o3, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.01", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.010", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.02", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder()); + } + { + IBundleProvider found = myObservationDao.search(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.001", "foo", "bar")); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder()); + } + } + @Test public void testChoiceParamString() { @@ -387,58 +416,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } - @Test - public void testCreateOperationOutcome() { - /* - * If any of this ever fails, it means that one of the OperationOutcome issue severity codes has changed code value across versions. We store the string as a constant, so something will need to - * be fixed. - */ - assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); - assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.ERROR.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); - assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); - assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); - assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.INFORMATION.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); - assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); - assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); - assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.WARNING.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); - assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); - } - - @Test - public void testCreateOperationOutcomeError() { - FhirResourceDaoDstu3 dao = new FhirResourceDaoDstu3(); - OperationOutcome oo = (OperationOutcome) dao.createErrorOperationOutcome("my message", "incomplete"); - assertEquals(IssueSeverity.ERROR.toCode(), oo.getIssue().get(0).getSeverity().toCode()); - assertEquals("my message", oo.getIssue().get(0).getDiagnostics()); - assertEquals(IssueType.INCOMPLETE, oo.getIssue().get(0).getCode()); - } - - @Test - public void testCreateOperationOutcomeInfo() { - FhirResourceDaoDstu3 dao = new FhirResourceDaoDstu3(); - OperationOutcome oo = (OperationOutcome) dao.createInfoOperationOutcome("my message"); - assertEquals(IssueSeverity.INFORMATION.toCode(), oo.getIssue().get(0).getSeverity().toCode()); - assertEquals("my message", oo.getIssue().get(0).getDiagnostics()); - assertEquals(IssueType.INFORMATIONAL, oo.getIssue().get(0).getCode()); - } - - @Test - public void testCreateSummaryFails() { - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue("testCreateTextIdFails"); - p.addName().addFamily("Hello"); - - ArrayList tl = new ArrayList(); - p.getMeta().addTag().setSystem(Constants.TAG_SUBSETTED_SYSTEM).setCode(Constants.TAG_SUBSETTED_CODE); - - try { - myPatientDao.create(p, new ServletRequestDetails()); - fail(); - } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("subsetted")); - } - } - @Test public void testCreateLongString() { //@formatter:off @@ -468,22 +445,57 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } @Test - public void testCreateWrongType() { + public void testCreateOperationOutcome() { + /* + * If any of this ever fails, it means that one of the OperationOutcome issue severity codes has changed code value across versions. We store the string as a constant, so something will need to + * be fixed. + */ + assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); + assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.ERROR.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); + assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR); + assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); + assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.INFORMATION.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); + assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO); + assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); + assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.WARNING.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); + assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN); + } - // Lose typing so we can put the wrong type in - @SuppressWarnings("rawtypes") - IFhirResourceDao dao = myNamingSystemDao; - - Patient resource = new Patient(); - resource.addName().addFamily("My Name"); - try { - dao.create(resource, new ServletRequestDetails()); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Incorrect resource type detected for endpoint, found Patient but expected NamingSystem", e.getMessage()); - } + @Test + public void testCreateOperationOutcomeError() { + FhirResourceDaoDstu3 dao = new FhirResourceDaoDstu3(); + OperationOutcome oo = (OperationOutcome) dao.createErrorOperationOutcome("my message", "incomplete"); + assertEquals(IssueSeverity.ERROR.toCode(), oo.getIssue().get(0).getSeverity().toCode()); + assertEquals("my message", oo.getIssue().get(0).getDiagnostics()); + assertEquals(IssueType.INCOMPLETE, oo.getIssue().get(0).getCode()); } + @Test + public void testCreateOperationOutcomeInfo() { + FhirResourceDaoDstu3 dao = new FhirResourceDaoDstu3(); + OperationOutcome oo = (OperationOutcome) dao.createInfoOperationOutcome("my message"); + assertEquals(IssueSeverity.INFORMATION.toCode(), oo.getIssue().get(0).getSeverity().toCode()); + assertEquals("my message", oo.getIssue().get(0).getDiagnostics()); + assertEquals(IssueType.INFORMATIONAL, oo.getIssue().get(0).getCode()); + } + + @Test + public void testCreateSummaryFails() { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("testCreateTextIdFails"); + p.addName().addFamily("Hello"); + + ArrayList tl = new ArrayList(); + p.getMeta().addTag().setSystem(Constants.TAG_SUBSETTED_SYSTEM).setCode(Constants.TAG_SUBSETTED_CODE); + + try { + myPatientDao.create(p, new ServletRequestDetails()); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("subsetted")); + } + } + @Test public void testCreateTextIdFails() { Patient p = new Patient(); @@ -629,12 +641,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } - private CodeableConcept newCodeableConcept(String theSystem, String theCode) { - CodeableConcept retVal = new CodeableConcept(); - retVal.addCoding().setSystem(theSystem).setCode(theCode); - return retVal; - } - @Test public void testCreateWithInvalidReferenceFailsGracefully() { Patient patient = new Patient(); @@ -691,6 +697,23 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } + @Test + public void testCreateWrongType() { + + // Lose typing so we can put the wrong type in + @SuppressWarnings("rawtypes") + IFhirResourceDao dao = myNamingSystemDao; + + Patient resource = new Patient(); + resource.addName().addFamily("My Name"); + try { + dao.create(resource, new ServletRequestDetails()); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Incorrect resource type detected for endpoint, found Patient but expected NamingSystem", e.getMessage()); + } + } + @Test public void testDatePeriodParamEndOnly() { { @@ -969,23 +992,40 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } @Test - public void testDeleteWithMatchUrlChainedString() { - String methodName = "testDeleteWithMatchUrlChainedString"; - - Organization org = new Organization(); - org.setName(methodName); - IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + public void testDeleteWithMatchUrl() { + String methodName = "testDeleteWithMatchUrl"; Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue(methodName); - p.getManagingOrganization().setReferenceElement(orgId); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId(); ourLog.info("Created patient, got it: {}", id); - myPatientDao.deleteByUrl("Patient?organization.name=" + methodName, new ServletRequestDetails()); + Bundle request = new Bundle(); + request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName); + + myPatientDao.deleteByUrl("Patient?identifier=urn%3Asystem%7C" + methodName, new ServletRequestDetails()); + + try { + myPatientDao.read(id.toVersionless(), new ServletRequestDetails()); + fail(); + } catch (ResourceGoneException e) { + // ok + } + + try { + myPatientDao.read(new IdType("Patient/" + methodName), new ServletRequestDetails()); + fail(); + } catch (ResourceNotFoundException e) { + // ok + } + + IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); + assertEquals(2, history.size()); + + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 1).get(0)).getValue()); + assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(1, 2).get(0))); - assertGone(id); } @Test @@ -1014,6 +1054,73 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testDeleteWithMatchUrlChainedProfile() { + String methodName = "testDeleteWithMatchUrlChainedProfile"; + + List profileList = new ArrayList(); + + Organization org = new Organization(); + + org.getMeta().getProfile().add(new IdType("http://foo")); + org.setName(methodName); + + IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.getManagingOrganization().setReferenceElement(orgId); + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + ourLog.info("Created patient, got it: {}", id); + + myPatientDao.deleteByUrl("Patient?organization._profile=http://foo", new ServletRequestDetails()); + assertGone(id); + + myOrganizationDao.deleteByUrl("Organization?_profile=http://foo", new ServletRequestDetails()); + try { + myOrganizationDao.read(orgId, new ServletRequestDetails()); + fail(); + } catch (ResourceGoneException e) { + // good + } + + try { + myPatientDao.deleteByUrl("Patient?organization._profile.identifier=http://foo", new ServletRequestDetails()); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid parameter chain: organization._profile.identifier", e.getMessage()); + } + + try { + myOrganizationDao.deleteByUrl("Organization?_profile.identifier=http://foo", new ServletRequestDetails()); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid parameter chain: _profile.identifier", e.getMessage()); + } + + } + + @Test + public void testDeleteWithMatchUrlChainedString() { + String methodName = "testDeleteWithMatchUrlChainedString"; + + Organization org = new Organization(); + org.setName(methodName); + IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.getManagingOrganization().setReferenceElement(orgId); + IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + ourLog.info("Created patient, got it: {}", id); + + myPatientDao.deleteByUrl("Patient?organization.name=" + methodName, new ServletRequestDetails()); + + assertGone(id); + } + @Test public void testDeleteWithMatchUrlChainedTag() { String methodName = "testDeleteWithMatchUrlChainedString"; @@ -1120,112 +1227,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { assertGone(org2Id); } - /** - * This gets called from assertGone too! Careful about exceptions... - */ - private void assertNotGone(IIdType theId) { - if ("Patient".equals(theId.getResourceType())) { - myPatientDao.read(theId, new ServletRequestDetails()); - } else if ("Organization".equals(theId.getResourceType())) { - myOrganizationDao.read(theId, new ServletRequestDetails()); - } else { - fail("No type"); - } - } - - private void assertGone(IIdType theId) { - try { - assertNotGone(theId); - fail(); - } catch (ResourceGoneException e) { - // good - } - } - - @Test - public void testDeleteWithMatchUrlChainedProfile() { - String methodName = "testDeleteWithMatchUrlChainedProfile"; - - List profileList = new ArrayList(); - - Organization org = new Organization(); - - org.getMeta().getProfile().add(new IdType("http://foo")); - org.setName(methodName); - - IIdType orgId = myOrganizationDao.create(org, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - p.getManagingOrganization().setReferenceElement(orgId); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - ourLog.info("Created patient, got it: {}", id); - - myPatientDao.deleteByUrl("Patient?organization._profile=http://foo", new ServletRequestDetails()); - assertGone(id); - - myOrganizationDao.deleteByUrl("Organization?_profile=http://foo", new ServletRequestDetails()); - try { - myOrganizationDao.read(orgId, new ServletRequestDetails()); - fail(); - } catch (ResourceGoneException e) { - // good - } - - try { - myPatientDao.deleteByUrl("Patient?organization._profile.identifier=http://foo", new ServletRequestDetails()); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Invalid parameter chain: organization._profile.identifier", e.getMessage()); - } - - try { - myOrganizationDao.deleteByUrl("Organization?_profile.identifier=http://foo", new ServletRequestDetails()); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Invalid parameter chain: _profile.identifier", e.getMessage()); - } - - } - - @Test - public void testDeleteWithMatchUrl() { - String methodName = "testDeleteWithMatchUrl"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - IIdType id = myPatientDao.create(p, new ServletRequestDetails()).getId(); - ourLog.info("Created patient, got it: {}", id); - - Bundle request = new Bundle(); - request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName); - - myPatientDao.deleteByUrl("Patient?identifier=urn%3Asystem%7C" + methodName, new ServletRequestDetails()); - - try { - myPatientDao.read(id.toVersionless(), new ServletRequestDetails()); - fail(); - } catch (ResourceGoneException e) { - // ok - } - - try { - myPatientDao.read(new IdType("Patient/" + methodName), new ServletRequestDetails()); - fail(); - } catch (ResourceNotFoundException e) { - // ok - } - - IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); - assertEquals(2, history.size()); - - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 0).get(0))); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 0).get(0)).getValue()); - assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(1, 1).get(0))); - - } - @Test public void testHistoryByForcedId() { IIdType idv1; @@ -1262,6 +1263,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { int halfSize = 50; int fullSize = 100; for (int i = 0; i < fullSize; i++) { + ourLog.info("Pass {}", i); if (i == halfSize) { Thread.sleep(fullSize); middleDate = new Date(); @@ -1398,6 +1400,57 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } + + @Test + public void testHistoryReflectsMetaOperations() throws Exception { + Patient inPatient = new Patient(); + inPatient.addName().addFamily("version1"); + inPatient.getMeta().addProfile("http://example.com/1"); + IIdType id = myPatientDao.create(inPatient, mySrd).getId().toUnqualifiedVersionless(); + + IBundleProvider history = myPatientDao.history(null, mySrd); + assertEquals(1, history.size()); + Patient outPatient = (Patient) history.getResources(0, 1).get(0); + assertEquals("version1", inPatient.getName().get(0).getFamilyAsSingleString()); + List profiles = toStringList(outPatient.getMeta().getProfile()); + assertThat(profiles, contains("http://example.com/1")); + + /* + * Change metadata + */ + + inPatient.getMeta().addProfile("http://example.com/2"); + myPatientDao.metaAddOperation(id, inPatient.getMeta(), mySrd); + + history = myPatientDao.history(null, mySrd); + assertEquals(1, history.size()); + outPatient = (Patient) history.getResources(0, 1).get(0); + assertEquals("version1", inPatient.getName().get(0).getFamilyAsSingleString()); + profiles = toStringList(outPatient.getMeta().getProfile()); + assertThat(profiles, containsInAnyOrder("http://example.com/1", "http://example.com/2")); + + /* + * Do an update + */ + + inPatient.setId(id); + inPatient.getMeta().addProfile("http://example.com/3"); + inPatient.getName().get(0).addFamily("version2"); + myPatientDao.update(inPatient, mySrd); + + history = myPatientDao.history(null, mySrd); + assertEquals(2, history.size()); + outPatient = (Patient) history.getResources(0, 2).get(0); + assertEquals("version1 version2", outPatient.getName().get(0).getFamilyAsSingleString()); + profiles = toStringList(outPatient.getMeta().getProfile()); + ourLog.info(profiles.toString()); + assertThat(profiles, containsInAnyOrder("http://example.com/1", "http://example.com/2", "http://example.com/3")); + + outPatient = (Patient) history.getResources(0, 2).get(1); + assertEquals("version1", outPatient.getName().get(0).getFamilyAsSingleString()); + profiles = toStringList(outPatient.getMeta().getProfile()); + assertThat(profiles, containsInAnyOrder("http://example.com/1", "http://example.com/2")); +} @Test public void testHistoryWithDeletedResource() throws Exception { @@ -1412,11 +1465,11 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { myPatientDao.update(patient, new ServletRequestDetails()); IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); - assertEquals(3, history.size()); List entries = history.getResources(0, 3); - ourLog.info("" + ((IAnyResource) entries.get(0)).getMeta().getLastUpdated()); - ourLog.info("" + ((IAnyResource) entries.get(1)).getMeta().getLastUpdated()); - ourLog.info("" + ((IAnyResource) entries.get(2)).getMeta().getLastUpdated()); + ourLog.info(((IAnyResource) entries.get(0)).getIdElement() + " - " + ((IAnyResource) entries.get(0)).getMeta().getLastUpdated()); + ourLog.info(((IAnyResource) entries.get(1)).getIdElement() + " - " + ((IAnyResource) entries.get(1)).getMeta().getLastUpdated()); + ourLog.info(((IAnyResource) entries.get(2)).getIdElement() + " - " + ((IAnyResource) entries.get(2)).getMeta().getLastUpdated()); + assertEquals(3, history.size()); assertEquals(id.withVersion("3"), entries.get(0).getIdElement()); assertEquals(id.withVersion("2"), entries.get(1).getIdElement()); @@ -1432,6 +1485,16 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { assertEquals(BundleEntryTransactionMethodEnum.POST.getCode(), ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get((IAnyResource) entries.get(2))); } + @Test + public void testHistoryWithInvalidId() throws Exception { + try { + myPatientDao.history(new IdDt("Patient/FOOFOOFOO"), null, new ServletRequestDetails()); + fail(); + } catch (ResourceNotFoundException e) { + assertEquals("Resource Patient/FOOFOOFOO is not known", e.getMessage()); + } + } + @Test public void testIdParam() { Patient patient = new Patient(); @@ -2327,79 +2390,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } - @Test - public void testSortByLastUpdated() { - String methodName = "testSortByLastUpdated"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system1").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system2").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id2 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system3").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id3 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system4").setValue(methodName); - p.addName().addFamily(methodName); - IIdType id4 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - SearchParameterMap pm; - List actual; - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id1, id2, id3, id4)); - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id1, id2, id3, id4)); - - pm = new SearchParameterMap(); - pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC)); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id4, id3, id2, id1)); - - pm = new SearchParameterMap(); - pm.add(Patient.SP_IDENTIFIER, new TokenParam(null, methodName)); - pm.setSort(new SortSpec(Patient.SP_NAME).setChain(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC))); - actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); - assertThat(actual, contains(id4, id3, id2, id1)); - } - - @Test - public void testSortNoMatches() { - String methodName = "testSortNoMatches"; - - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); - - SearchParameterMap map; - - map = new SearchParameterMap(); - map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); - map.setLastUpdated(new DateRangeParam("2001", "2003")); - map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); - assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); - - map = new SearchParameterMap(); - map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); - map.setLastUpdated(new DateRangeParam("2001", "2003")); - map.setSort(new SortSpec(Patient.SP_NAME)); - assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); - - } - @Test public void testSortById() { String methodName = "testSortBTyId"; @@ -2451,6 +2441,55 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { assertThat(actual, contains(id4, id3, id2, id1, idMethodName)); } + @Test + public void testSortByLastUpdated() { + String methodName = "testSortByLastUpdated"; + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system1").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system2").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id2 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system3").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id3 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system4").setValue(methodName); + p.addName().addFamily(methodName); + IIdType id4 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + SearchParameterMap pm; + List actual; + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC)); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id4, id3, id2, id1)); + + pm = new SearchParameterMap(); + pm.add(Patient.SP_IDENTIFIER, new TokenParam(null, methodName)); + pm.setSort(new SortSpec(Patient.SP_NAME).setChain(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC))); + actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm)); + assertThat(actual, contains(id4, id3, id2, id1)); + } + @Test public void testSortByNumber() { String methodName = "testSortByNumber"; @@ -2778,6 +2817,30 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testSortNoMatches() { + String methodName = "testSortNoMatches"; + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + IIdType id1 = myPatientDao.create(p, new ServletRequestDetails()).getId().toUnqualifiedVersionless(); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); + map.setLastUpdated(new DateRangeParam("2001", "2003")); + map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); + + map = new SearchParameterMap(); + map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart())); + map.setLastUpdated(new DateRangeParam("2001", "2003")); + map.setSort(new SortSpec(Patient.SP_NAME)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty()); + + } + @Test public void testStoreUnversionedResources() { Organization o1 = new Organization(); @@ -2994,4 +3057,12 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } } + private static List toStringList(List theUriType) { + ArrayList retVal = new ArrayList(); + for (UriType next : theUriType) { + retVal.add(next.getValue()); + } + return retVal; + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index f79375cd079..8d70cffa9e7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -852,9 +852,9 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { IBundleProvider history = myPatientDao.history(id, null, new ServletRequestDetails()); assertEquals(2, history.size()); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 0).get(0))); - assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 0).get(0)).getValue()); - assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(1, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 1).get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(0, 1).get(0)).getValue()); + assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) history.getResources(1, 2).get(0))); } diff --git a/hapi-fhir-validation-resources-dstu3/.classpath b/hapi-fhir-validation-resources-dstu3/.classpath index a9eab599e39..899c8e120ef 100644 --- a/hapi-fhir-validation-resources-dstu3/.classpath +++ b/hapi-fhir-validation-resources-dstu3/.classpath @@ -1,6 +1,7 @@ + diff --git a/hapi-fhir-validation-resources-dstu3/src/main/java/.keep b/hapi-fhir-validation-resources-dstu3/src/main/java/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 121627610db..12d88795ea5 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -191,6 +191,17 @@ Old searches are deleted after an hour by default, but this can be changed via a setting in the DaoConfig. + + JPA servers' resource version history mechanism + has been adjusted so that the history table + keeps a record of all versions including the + current version. This has the very helpful + side effect that history no longer needs to be + paged into memory as a complete set. Previously + history had a hard limit of only being able to + page the most recent 20000 entries. Now it has + no limit. +