diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 97feca2e281..e82661f5ce4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.api; */ import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.*; public class Constants { @@ -168,6 +169,7 @@ public class Constants { public static final String PARAM_SORT = "_sort"; public static final String PARAM_SORT_ASC = "_sort:asc"; public static final String PARAM_SORT_DESC = "_sort:desc"; + public static final String PARAM_SOURCE = "_source"; public static final String PARAM_SUMMARY = "_summary"; public static final String PARAM_TAG = "_tag"; public static final String PARAM_TAGS = "_tags"; @@ -220,10 +222,14 @@ public class Constants { public static final String CACHE_CONTROL_PRIVATE = "private"; public static final int STATUS_HTTP_412_PAYLOAD_TOO_LARGE = 413; public static final String OPERATION_NAME_GRAPHQL = "$graphql"; + /** + * Note that this constant is used in a number of places including DB column lengths! Be careful if you decide to change it. + */ + public static final int REQUEST_ID_LENGTH = 16; static { - CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); - CHARSET_US_ASCII = Charset.forName("ISO-8859-1"); + CHARSET_UTF8 = StandardCharsets.UTF_8; + CHARSET_US_ASCII = StandardCharsets.ISO_8859_1; HashMap statusNames = new HashMap<>(); statusNames.put(200, "OK"); @@ -239,7 +245,6 @@ public class Constants { statusNames.put(300, "Multiple Choices"); statusNames.put(301, "Moved Permanently"); statusNames.put(302, "Found"); - statusNames.put(302, "Moved Temporarily"); statusNames.put(303, "See Other"); statusNames.put(304, "Not Modified"); statusNames.put(305, "Use Proxy"); @@ -259,9 +264,7 @@ public class Constants { statusNames.put(411, "Length Required"); statusNames.put(412, "Precondition Failed"); statusNames.put(413, "Payload Too Large"); - statusNames.put(413, "Request Entity Too Large"); statusNames.put(414, "URI Too Long"); - statusNames.put(414, "Request-URI Too Long"); statusNames.put(415, "Unsupported Media Type"); statusNames.put(416, "Requested range not satisfiable"); statusNames.put(417, "Expectation Failed"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java new file mode 100644 index 00000000000..2fd2d20bdad --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import java.util.List; + +public class MetaUtil { + + private MetaUtil() { + // non-instantiable + } + + public static String getSource(FhirContext theContext, IBaseMetaType theMeta) { + BaseRuntimeElementCompositeDefinition elementDef = (BaseRuntimeElementCompositeDefinition) theContext.getElementDefinition(theMeta.getClass()); + BaseRuntimeChildDefinition sourceChild = elementDef.getChildByName("source"); + List sourceValues = sourceChild.getAccessor().getValues(theMeta); + String retVal = null; + if (sourceValues.size() > 0) { + retVal = ((IPrimitiveType) sourceValues.get(0)).getValueAsString(); + } + return retVal; + } + + public static void setSource(FhirContext theContext, IBaseMetaType theMeta, String theValue) { + BaseRuntimeElementCompositeDefinition elementDef = (BaseRuntimeElementCompositeDefinition) theContext.getElementDefinition(theMeta.getClass()); + BaseRuntimeChildDefinition sourceChild = elementDef.getChildByName("source"); + List sourceValues = sourceChild.getAccessor().getValues(theMeta); + IPrimitiveType sourceElement; + if (sourceValues.size() > 0) { + sourceElement = ((IPrimitiveType) sourceValues.get(0)); + } else { + sourceElement = (IPrimitiveType) theContext.getElementDefinition("uri").newInstance(); + sourceChild.getMutator().setValue(theMeta, sourceElement); + } + sourceElement.setValueAsString(theValue); + } + +} diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseMetaType.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseMetaType.java index a41862a044c..8ddb333e125 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseMetaType.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseMetaType.java @@ -57,4 +57,5 @@ public interface IBaseMetaType extends ICompositeType { */ IBaseCoding getSecurity(String theSystem, String theCode); + } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 542cc62522e..ed76533ec3b 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -105,6 +105,7 @@ ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPa ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1} ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1} +ca.uhn.fhir.jpa.dao.SearchBuilder.sourceParamDisabled=The _source parameter is disabled on this server ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found! ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found! 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 dd76b67e210..602daaeb653 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 @@ -51,6 +51,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.CoverageIgnore; +import ca.uhn.fhir.util.MetaUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.XmlUtil; import com.google.common.annotations.VisibleForTesting; @@ -130,6 +131,8 @@ public abstract class BaseHapiFhirDao implements IDao, @Autowired protected IForcedIdDao myForcedIdDao; @Autowired + protected IResourceProvenanceDao myResourceProvenanceDao; + @Autowired protected ISearchCoordinatorSvc mySearchCoordinatorSvc; @Autowired protected ISearchParamRegistry mySerarchParamRegistry; @@ -282,6 +285,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } } + } private void findMatchingTagIds(RequestDetails theRequest, String theResourceName, IIdType theResourceId, Set tagIds, Class entityClass) { @@ -611,7 +615,10 @@ public abstract class BaseHapiFhirDao implements IDao, if (theEntity.getId() == null) { changed = true; } else { - ResourceHistoryTable currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion()); + ResourceHistoryTable currentHistoryVersion = theEntity.getCurrentVersionEntity(); + if (currentHistoryVersion == null) { + currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), theEntity.getVersion()); + } if (currentHistoryVersion == null || currentHistoryVersion.getResource() == null) { changed = true; } else { @@ -832,11 +839,7 @@ public abstract class BaseHapiFhirDao implements IDao, metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE); } - if (metaSnapshotModeTokens.contains(theTag.getTag().getTagType())) { - return true; - } - - return false; + return metaSnapshotModeTokens.contains(theTag.getTag().getTagType()); } @Override @@ -851,10 +854,12 @@ public abstract class BaseHapiFhirDao implements IDao, public R toResource(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation) { // 1. get resource, it's encoding and the tags if any - byte[] resourceBytes = null; - ResourceEncodingEnum resourceEncoding = null; - Collection myTagList = null; - Long version = null; + byte[] resourceBytes; + ResourceEncodingEnum resourceEncoding; + Collection myTagList; + Long version; + String provenanceSourceUri = null; + String provenanceRequestId = null; if (theEntity instanceof ResourceHistoryTable) { ResourceHistoryTable history = (ResourceHistoryTable) theEntity; @@ -862,14 +867,20 @@ public abstract class BaseHapiFhirDao implements IDao, resourceEncoding = history.getEncoding(); myTagList = history.getTags(); version = history.getVersion(); + if (history.getProvenance() != null) { + provenanceRequestId = history.getProvenance().getRequestId(); + provenanceSourceUri = history.getProvenance().getSourceUri(); + } } else if (theEntity instanceof ResourceTable) { ResourceTable resource = (ResourceTable) theEntity; version = theEntity.getVersion(); - ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version); + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version); + ((ResourceTable)theEntity).setCurrentVersionEntity(history); + while (history == null) { if (version > 1L) { version--; - history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version); + history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version); } else { return null; } @@ -878,12 +889,18 @@ public abstract class BaseHapiFhirDao implements IDao, resourceEncoding = history.getEncoding(); myTagList = resource.getTags(); version = history.getVersion(); + if (history.getProvenance() != null) { + provenanceRequestId = history.getProvenance().getRequestId(); + provenanceSourceUri = history.getProvenance().getSourceUri(); + } } else if (theEntity instanceof ResourceSearchView) { // This is the search View - ResourceSearchView myView = (ResourceSearchView) theEntity; - resourceBytes = myView.getResource(); - resourceEncoding = myView.getEncoding(); - version = myView.getVersion(); + ResourceSearchView view = (ResourceSearchView) theEntity; + resourceBytes = view.getResource(); + resourceEncoding = view.getEncoding(); + version = view.getVersion(); + provenanceRequestId = view.getProvenanceRequestId(); + provenanceSourceUri = view.getProvenanceSourceUri(); if (theTagList == null) myTagList = new HashSet<>(); else @@ -954,6 +971,23 @@ public abstract class BaseHapiFhirDao implements IDao, retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version); } + // 6. Handle source (provenance) + if (isNotBlank(provenanceRequestId) || isNotBlank(provenanceSourceUri)) { + String sourceString = defaultString(provenanceSourceUri) + + (isNotBlank(provenanceRequestId) ? "#" : "") + + defaultString(provenanceRequestId); + + if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { + IBaseExtension sourceExtension = ((IBaseHasExtensions) retVal.getMeta()).addExtension(); + sourceExtension.setUrl(JpaConstants.EXT_META_SOURCE); + IPrimitiveType value = (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); + value.setValue(sourceString); + sourceExtension.setValue(value); + } else if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { + MetaUtil.setSource(myContext, retVal.getMeta(), sourceString); + } + } + return retVal; } @@ -1097,6 +1131,40 @@ public abstract class BaseHapiFhirDao implements IDao, ourLog.debug("Saving history entry {}", historyEntry.getIdDt()); myResourceHistoryTableDao.save(historyEntry); + + // Save resource source + String source = null; + String requestId = theRequest != null ? theRequest.getRequestId() : null; + if (theResource != null) { + if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { + IBaseMetaType meta = theResource.getMeta(); + source = MetaUtil.getSource(myContext, meta); + } + if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { + source = ((IBaseHasExtensions) theResource.getMeta()) + .getExtension() + .stream() + .filter(t -> JpaConstants.EXT_META_SOURCE.equals(t.getUrl())) + .filter(t -> t.getValue() instanceof IPrimitiveType) + .map(t -> ((IPrimitiveType) t.getValue()).getValueAsString()) + .findFirst() + .orElse(null); + } + } + boolean haveSource = isNotBlank(source) && myConfig.getStoreMetaSourceInformation().isStoreSourceUri(); + boolean haveRequestId = isNotBlank(requestId) && myConfig.getStoreMetaSourceInformation().isStoreRequestId(); + if (haveSource || haveRequestId) { + ResourceHistoryProvenanceEntity provenance = new ResourceHistoryProvenanceEntity(); + provenance.setResourceHistoryTable(historyEntry); + provenance.setResourceTable(theEntity); + if (haveRequestId) { + provenance.setRequestId(left(requestId, Constants.REQUEST_ID_LENGTH)); + } + if (haveSource) { + provenance.setSourceUri(source); + } + myEntityManager.persist(provenance); + } } /* 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 443b9587808..f638c26be59 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 @@ -269,7 +269,10 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) { DeleteConflictList deleteConflicts = new DeleteConflictList(); - deleteConflicts.setResourceIdMarkedForDeletion(theId); + if (theId != null && isNotBlank(theId.getValue())) { + deleteConflicts.setResourceIdMarkedForDeletion(theId); + } + StopWatch w = new StopWatch(); DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails); @@ -673,7 +676,7 @@ public abstract class BaseHapiFhirResourceDao extends B doMetaAdd(theMetaAdd, latestVersion); // Also update history entry - ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion()); + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion()); doMetaAdd(theMetaAdd, history); } @@ -705,7 +708,7 @@ public abstract class BaseHapiFhirResourceDao extends B doMetaDelete(theMetaDel, latestVersion); // Also update history entry - ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion()); + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion()); doMetaDelete(theMetaDel, history); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 53f4506efff..18ae898014a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -151,6 +151,7 @@ public class DaoConfig { */ private boolean myPreExpandValueSetsExperimental = false; private boolean myFilterParameterEnabled = false; + private StoreMetaSourceInformation myStoreMetaSourceInformation = StoreMetaSourceInformation.SOURCE_URI_AND_REQUEST_ID; /** * Constructor @@ -974,6 +975,7 @@ public class DaoConfig { * and other FHIR features may not behave as expected when referential integrity is not * preserved. Use this feature with caution. *

+ * * @see CascadingDeleteInterceptor */ public boolean isEnforceReferentialIntegrityOnDelete() { @@ -988,6 +990,7 @@ public class DaoConfig { * and other FHIR features may not behave as expected when referential integrity is not * preserved. Use this feature with caution. *

+ * * @see CascadingDeleteInterceptor */ public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) { @@ -1088,16 +1091,16 @@ public class DaoConfig { * The expunge batch size (default 800) determines the number of records deleted within a single transaction by the * expunge operation. */ - public void setExpungeBatchSize(int theExpungeBatchSize) { - myExpungeBatchSize = theExpungeBatchSize; + public int getExpungeBatchSize() { + return myExpungeBatchSize; } /** * The expunge batch size (default 800) determines the number of records deleted within a single transaction by the * expunge operation. */ - public int getExpungeBatchSize() { - return myExpungeBatchSize; + public void setExpungeBatchSize(int theExpungeBatchSize) { + myExpungeBatchSize = theExpungeBatchSize; } /** @@ -1656,6 +1659,54 @@ public class DaoConfig { myFilterParameterEnabled = theFilterParameterEnabled; } + /** + * If enabled, resource source information (Resource.meta.source) will be persisted along with + * each resource. This adds extra table and index space so it should be disabled if it is not being + * used. + *

+ * Default is {@link StoreMetaSourceInformation#SOURCE_URI_AND_REQUEST_ID} + *

+ */ + public StoreMetaSourceInformation getStoreMetaSourceInformation() { + return myStoreMetaSourceInformation; + } + + /** + * If enabled, resource source information (Resource.meta.source) will be persisted along with + * each resource. This adds extra table and index space so it should be disabled if it is not being + * used. + *

+ * Default is {@link StoreMetaSourceInformation#SOURCE_URI_AND_REQUEST_ID} + *

+ */ + public void setStoreMetaSourceInformation(StoreMetaSourceInformation theStoreMetaSourceInformation) { + Validate.notNull(theStoreMetaSourceInformation, "theStoreMetaSourceInformation must not be null"); + myStoreMetaSourceInformation = theStoreMetaSourceInformation; + } + + public enum StoreMetaSourceInformation { + NONE(false, false), + SOURCE_URI(true, false), + REQUEST_ID(false, true), + SOURCE_URI_AND_REQUEST_ID(true, true); + + private final boolean myStoreSourceUri; + private final boolean myStoreRequestId; + + StoreMetaSourceInformation(boolean theStoreSourceUri, boolean theStoreRequestId) { + myStoreSourceUri = theStoreSourceUri; + myStoreRequestId = theStoreRequestId; + } + + public boolean isStoreSourceUri() { + return myStoreSourceUri; + } + + public boolean isStoreRequestId() { + return myStoreRequestId; + } + } + public enum IndexEnabledEnum { ENABLED, DISABLED 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 7d17c84922d..1e16b8d7ea2 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 @@ -823,6 +823,48 @@ public class SearchBuilder implements ISearchBuilder { return null; } + + private Predicate addPredicateSource(List theList, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest) { + if (myDaoConfig.getStoreMetaSourceInformation() == DaoConfig.StoreMetaSourceInformation.NONE) { + String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "sourceParamDisabled"); + throw new InvalidRequestException(msg); + } + + Join join = myResourceTableRoot.join("myProvenance", JoinType.LEFT); + + List codePredicates = new ArrayList<>(); + + for (IQueryParameterType nextParameter : theList) { + String nextParamValue = nextParameter.getValueAsQueryToken(myContext); + int lastHashValueIndex = nextParamValue.lastIndexOf('#'); + String sourceUri; + String requestId; + if (lastHashValueIndex == -1) { + sourceUri = nextParamValue; + requestId = null; + } else { + sourceUri = nextParamValue.substring(0, lastHashValueIndex); + requestId = nextParamValue.substring(lastHashValueIndex + 1); + } + requestId = left(requestId, Constants.REQUEST_ID_LENGTH); + + Predicate sourceUriPredicate = myBuilder.equal(join.get("mySourceUri"), sourceUri); + Predicate requestIdPredicate = myBuilder.equal(join.get("myRequestId"), requestId); + if (isNotBlank(sourceUri) && isNotBlank(requestId)) { + codePredicates.add(myBuilder.and(sourceUriPredicate, requestIdPredicate)); + } else if (isNotBlank(sourceUri)) { + codePredicates.add(sourceUriPredicate); + } else if (isNotBlank(requestId)) { + codePredicates.add(requestIdPredicate); + } + } + + Predicate retVal = myBuilder.or(toArray(codePredicates)); + myPredicates.add(retVal); + return retVal; + } + + private Predicate addPredicateString(String theResourceName, String theParamName, List theList) { @@ -2680,6 +2722,14 @@ public class SearchBuilder implements ISearchBuilder { } else { throw new InvalidRequestException("Unexpected search parameter type encountered, expected string type for language search"); } + } else if (searchParam.getName().equals(Constants.PARAM_SOURCE)) { + if (searchParam.getParamType() == RestSearchParameterTypeEnum.TOKEN) { + TokenParam param = new TokenParam(); + param.setValueAsQueryToken(null, null, null, theFilter.getValue()); + return addPredicateSource(Collections.singletonList(param), theFilter.getOperation(), theRequest); + } else { + throw new InvalidRequestException("Unexpected search parameter type encountered, expected token type for _id search"); + } } // else if ((searchParam.getName().equals(Constants.PARAM_TAG)) || // (searchParam.equals(Constants.PARAM_SECURITY))) { @@ -2798,6 +2848,12 @@ public class SearchBuilder implements ISearchBuilder { addPredicateTag(theAndOrParams, theParamName); + } else if (theParamName.equals(Constants.PARAM_SOURCE)) { + + for (List nextAnd : theAndOrParams) { + addPredicateSource(nextAnd, SearchFilterParser.CompareOperation.eq, theRequest); + } + } else { RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); 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 index 9ecaa1480ab..d4dda01f835 100644 --- 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 @@ -66,8 +66,8 @@ public interface IResourceHistoryTableDao extends JpaRepository findForResourceId(Pageable thePage, @Param("resId") Long theId, @Param("dontWantVersion") Long theDontWantVersion); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceProvenanceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceProvenanceDao.java new file mode 100644 index 00000000000..3b301516ea5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceProvenanceDao.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IResourceProvenanceDao extends JpaRepository { + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index 083f7f25ba3..673ef3a9f49 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -120,6 +120,7 @@ public class ExpungeEverythingService { counter.addAndGet(expungeEverythingByType(ResourceHistoryTag.class)); counter.addAndGet(expungeEverythingByType(ResourceTag.class)); counter.addAndGet(expungeEverythingByType(TagDefinition.class)); + counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class)); counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class)); counter.addAndGet(expungeEverythingByType(ResourceTable.class)); myTxTemplate.execute(t -> { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java index 051e171704b..5f5203a055f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java @@ -87,6 +87,8 @@ class ResourceExpungeService implements IResourceExpungeService { private IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired private DaoRegistry myDaoRegistry; + @Autowired + private IResourceProvenanceDao myResourceHistoryProvenanceTableDao; @Override @Transactional @@ -94,7 +96,7 @@ class ResourceExpungeService implements IResourceExpungeService { Pageable page = PageRequest.of(0, theRemainingCount); if (theResourceId != null) { if (theVersion != null) { - return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion)); + return toSlice(myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theResourceId, theVersion)); } else { return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theResourceId); } @@ -146,6 +148,10 @@ class ResourceExpungeService implements IResourceExpungeService { callHooks(theRequestDetails, theRemainingCount, version, id); + if (version.getProvenance() != null) { + myResourceHistoryProvenanceTableDao.delete(version.getProvenance()); + } + myResourceHistoryTagDao.deleteAll(version.getTags()); myResourceHistoryTableDao.delete(version); @@ -194,7 +200,7 @@ class ResourceExpungeService implements IResourceExpungeService { private void expungeCurrentVersionOfResource(RequestDetails theRequestDetails, Long theResourceId, AtomicInteger theRemainingCount) { ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new); - ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion()); + ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resource.getId(), resource.getVersion()); if (currentVersion != null) { expungeHistoricalVersion(theRequestDetails, currentVersion.getId(), theRemainingCount); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java index 60515aaa4bf..3d94ee48daf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.Constants; @@ -34,25 +35,26 @@ import javax.persistence.*; import java.io.Serializable; import java.util.Date; -//@formatter:off @Entity @Immutable -@Subselect("SELECT h.pid as pid " + - ", h.res_id as res_id " + - ", h.res_type as res_type " + - ", h.res_version as res_version " + // FHIR version - ", h.res_ver as res_ver " + // resource version - ", h.has_tags as has_tags " + - ", h.res_deleted_at as res_deleted_at " + - ", h.res_published as res_published " + - ", h.res_updated as res_updated " + - ", h.res_text as res_text " + - ", h.res_encoding as res_encoding " + - ", f.forced_id as FORCED_PID " + +@Subselect("SELECT h.pid as pid, " + + " h.res_id as res_id, " + + " h.res_type as res_type, " + + " h.res_version as res_version, " + // FHIR version + " h.res_ver as res_ver, " + // resource version + " h.has_tags as has_tags, " + + " h.res_deleted_at as res_deleted_at, " + + " h.res_published as res_published, " + + " h.res_updated as res_updated, " + + " h.res_text as res_text, " + + " h.res_encoding as res_encoding, " + + " p.SOURCE_URI as PROV_SOURCE_URI," + + " p.REQUEST_ID as PROV_REQUEST_ID," + + " f.forced_id as FORCED_PID " + "FROM HFJ_RES_VER h " + " LEFT OUTER JOIN HFJ_FORCED_ID f ON f.resource_pid = h.res_id " + + " LEFT OUTER JOIN HFJ_RES_VER_PROV p ON p.res_ver_pid = h.pid " + " INNER JOIN HFJ_RESOURCE r ON r.res_id = h.res_id and r.res_ver = h.res_ver") -// @formatter:on public class ResourceSearchView implements IBaseResourceEntity, Serializable { private static final long serialVersionUID = 1L; @@ -73,36 +75,41 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable { @Column(name = "RES_VER") private Long myResourceVersion; - + @Column(name = "PROV_REQUEST_ID", length = Constants.REQUEST_ID_LENGTH) + private String myProvenanceRequestId; + @Column(name = "PROV_SOURCE_URI", length = ResourceHistoryProvenanceEntity.SOURCE_URI_LENGTH) + private String myProvenanceSourceUri; @Column(name = "HAS_TAGS") private boolean myHasTags; - @Column(name = "RES_DELETED_AT") @Temporal(TemporalType.TIMESTAMP) private Date myDeleted; - @Temporal(TemporalType.TIMESTAMP) @Column(name = "RES_PUBLISHED") private Date myPublished; - @Temporal(TemporalType.TIMESTAMP) @Column(name = "RES_UPDATED") private Date myUpdated; - @Column(name = "RES_TEXT") @Lob() private byte[] myResource; - @Column(name = "RES_ENCODING") @Enumerated(EnumType.STRING) private ResourceEncodingEnum myEncoding; - - @Column(name = "FORCED_PID", length= ForcedId.MAX_FORCED_ID_LENGTH) + @Column(name = "FORCED_PID", length = ForcedId.MAX_FORCED_ID_LENGTH) private String myForcedPid; public ResourceSearchView() { } + public String getProvenanceRequestId() { + return myProvenanceRequestId; + } + + public String getProvenanceSourceUri() { + return myProvenanceSourceUri; + } + @Override public Date getDeleted() { return myDeleted; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index b7787adcd4d..6a05a26dae7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -159,7 +159,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe .stream() .map(CircularQueueCaptureQueriesListener::formatQueryAsSql) .collect(Collectors.toList()); - ourLog.info("Select Queries:\n{}", String.join("\n", queries)); + ourLog.info("Update Queries:\n{}", String.join("\n", queries)); } /** diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SourceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SourceTest.java new file mode 100644 index 00000000000..0e9a32c0b58 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SourceTest.java @@ -0,0 +1,234 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import org.apache.commons.text.RandomStringGenerator; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +@SuppressWarnings({"Duplicates"}) +public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test { + + @After + public final void after() { + when(mySrd.getRequestId()).thenReturn(null); + myDaoConfig.setStoreMetaSourceInformation(new DaoConfig().getStoreMetaSourceInformation()); + } + + @Before + public void before() { + myDaoConfig.setStoreMetaSourceInformation(DaoConfig.StoreMetaSourceInformation.SOURCE_URI_AND_REQUEST_ID); + } + + @Test + public void testSourceStoreAndSearch() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + // Search by source URI + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("urn:source:0")); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + pt0 = (Patient) result.getResources(0, 1).get(0); + assertEquals("urn:source:0#a_request_id", pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); + + // Search by request ID + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("#a_request_id")); + result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue(), pt1id.getValue())); + + // Search by source URI and request ID + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("urn:source:0#a_request_id")); + result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + + @Test + public void testSearchWithOr() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:2")); + pt2.setActive(true); + myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenOrListParam() + .addOr(new TokenParam("urn:source:0")) + .addOr(new TokenParam("urn:source:1"))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue(), pt1id.getValue())); + + } + + @Test + public void testSearchWithAnd() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:2")); + pt2.setActive(true); + myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("@a_request_id"))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + @Test + public void testSearchLongRequestId() { + String requestId = new RandomStringGenerator.Builder().build().generate(5000); + when(mySrd.getRequestId()).thenReturn(requestId); + + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("#" + requestId))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + @Test + public void testSourceNotPreservedAcrossUpdate() { + + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + pt0 = myPatientDao.read(pt0id); + assertEquals("urn:source:0", pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); + + pt0.getMeta().getExtension().clear(); + pt0.setActive(false); + myPatientDao.update(pt0); + + pt0 = myPatientDao.read(pt0id.withVersion("2")); + assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); + + } + + @Test + public void testSourceDisabled() { + myDaoConfig.setStoreMetaSourceInformation(DaoConfig.StoreMetaSourceInformation.NONE); + when(mySrd.getRequestId()).thenReturn("0000000000000000"); + + Patient pt0 = new Patient(); + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + pt0 = myPatientDao.read(pt0id); + assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); + + pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); + pt0.setActive(false); + myPatientDao.update(pt0); + + pt0 = myPatientDao.read(pt0id.withVersion("2")); + assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); + + // Search without source param + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + // Search with source param + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("@a_request_id"))); + try { + myPatientDao.search(params); + } catch (InvalidRequestException e) { + assertEquals(e.getMessage(), "The _source parameter is disabled on this server"); + } + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + public static void assertConflictException(String theResourceType, ResourceVersionConflictException e) { + assertThat(e.getMessage(), matchesPattern( + "Unable to delete [a-zA-Z]+/[0-9]+ because at least one resource has a reference to this resource. First reference found was resource " + theResourceType + "/[0-9]+ in path [a-zA-Z]+.[a-zA-Z]+")); + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java index 72f0d32ee7e..f0a1c5acee8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java @@ -2,14 +2,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.util.TestUtil; -import com.google.common.base.Charsets; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpDelete; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.junit.AfterClass; @@ -19,7 +14,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.*; public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { @@ -42,11 +36,11 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { // Current version should be marked as deleted runInTransaction(()->{ - ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 1); + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 1); assertNull(resourceTable.getDeleted()); }); runInTransaction(()->{ - ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 2); + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 2); assertNotNull(resourceTable.getDeleted()); }); @@ -169,7 +163,7 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { // Mark the current history version as not-deleted even though the actual resource // table entry is marked deleted runInTransaction(()->{ - ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 2); + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 2); resourceTable.setDeleted(null); myResourceHistoryTableDao.save(resourceTable); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java new file mode 100644 index 00000000000..dbfb0052895 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -0,0 +1,144 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.junit.*; +import org.springframework.test.context.TestPropertySource; + +import java.util.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.reset; + +@TestPropertySource(properties = { + "scheduling_disabled=true" +}) +public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); + + @After + public void afterResetDao() { + myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit()); + myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); + } + + @Before + public void before() { + myInterceptorRegistry.registerInterceptor(myInterceptor); + } + + + @Test + public void testUpdateWithNoChanges() { + IIdType id = runInTransaction(() -> { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("2"); + return myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + Patient p = new Patient(); + p.setId(id.getIdPart()); + p.addIdentifier().setSystem("urn:system").setValue("2"); + myPatientDao.update(p).getResource(); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(5, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + assertThat(myCaptureQueriesListener.getInsertQueriesForCurrentThread(), empty()); + assertThat(myCaptureQueriesListener.getDeleteQueriesForCurrentThread(), empty()); + } + + + @Test + public void testUpdateWithChanges() { + IIdType id = runInTransaction(() -> { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("2"); + return myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + Patient p = new Patient(); + p.setId(id.getIdPart()); + p.addIdentifier().setSystem("urn:system").setValue("3"); + myPatientDao.update(p).getResource(); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(6, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + + @Test + public void testRead() { + IIdType id = runInTransaction(() -> { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("2"); + return myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + myPatientDao.read(id.toVersionless()); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + + @Test + public void testVRead() { + IIdType id = runInTransaction(() -> { + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("2"); + return myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + myPatientDao.read(id.withVersion("1")); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index d83377face9..1b4c239f66c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -1298,6 +1298,34 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } + @Test + public void testCustomCodeableConcept() { + SearchParameter fooSp = new SearchParameter(); + fooSp.addBase("ChargeItem"); + fooSp.setName("Product"); + fooSp.setCode("product"); + fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); + fooSp.setTitle("Product within a ChargeItem"); + fooSp.setExpression("ChargeItem.product.as(CodeableConcept)"); + fooSp.setStatus(Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(fooSp, mySrd); + + mySearchParamRegistry.forceRefresh(); + + ChargeItem ci = new ChargeItem(); + ci.setProduct(new CodeableConcept()); + ci.getProductCodeableConcept().addCoding().setCode("1"); + myChargeItemDao.create(ci); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add("product", new TokenParam(null, "1")); + IBundleProvider results = myChargeItemDao.search(map); + assertEquals(1, results.size().intValue()); + + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SourceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SourceTest.java new file mode 100644 index 00000000000..10e51c05432 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SourceTest.java @@ -0,0 +1,234 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import org.apache.commons.text.RandomStringGenerator; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import java.util.UUID; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +@SuppressWarnings({"Duplicates"}) +public class FhirResourceDaoR4SourceTest extends BaseJpaR4Test { + + @After + public final void after() { + when(mySrd.getRequestId()).thenReturn(null); + myDaoConfig.setStoreMetaSourceInformation(new DaoConfig().getStoreMetaSourceInformation()); + } + + @Before + public void before() { + myDaoConfig.setStoreMetaSourceInformation(DaoConfig.StoreMetaSourceInformation.SOURCE_URI_AND_REQUEST_ID); + } + + @Test + public void testSourceStoreAndSearch() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().setSource("urn:source:1"); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + // Search by source URI + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("urn:source:0")); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + pt0 = (Patient) result.getResources(0, 1).get(0); + assertEquals("urn:source:0#a_request_id", pt0.getMeta().getSource()); + + // Search by request ID + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("#a_request_id")); + result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue(), pt1id.getValue())); + + // Search by source URI and request ID + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenParam("urn:source:0#a_request_id")); + result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + + @Test + public void testSearchWithOr() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().setSource("urn:source:1"); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.getMeta().setSource("urn:source:2"); + pt2.setActive(true); + myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenOrListParam() + .addOr(new TokenParam("urn:source:0")) + .addOr(new TokenParam("urn:source:1"))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue(), pt1id.getValue())); + + } + + @Test + public void testSearchWithAnd() { + String requestId = "a_request_id"; + + when(mySrd.getRequestId()).thenReturn(requestId); + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt1 = new Patient(); + pt1.getMeta().setSource("urn:source:1"); + pt1.setActive(true); + IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.getMeta().setSource("urn:source:2"); + pt2.setActive(true); + myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("@a_request_id"))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + @Test + public void testSearchLongRequestId() { + String requestId = new RandomStringGenerator.Builder().build().generate(5000); + when(mySrd.getRequestId()).thenReturn(requestId); + + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + // Search + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("#" + requestId))); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + } + + @Test + public void testSourceNotPreservedAcrossUpdate() { + + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + pt0 = myPatientDao.read(pt0id); + assertEquals("urn:source:0", pt0.getMeta().getSource()); + + pt0.getMeta().setSource(null); + pt0.setActive(false); + myPatientDao.update(pt0); + + pt0 = myPatientDao.read(pt0id.withVersion("2")); + assertEquals(null, pt0.getMeta().getSource()); + + } + + @Test + public void testSourceDisabled() { + myDaoConfig.setStoreMetaSourceInformation(DaoConfig.StoreMetaSourceInformation.NONE); + when(mySrd.getRequestId()).thenReturn("0000000000000000"); + + Patient pt0 = new Patient(); + pt0.getMeta().setSource("urn:source:0"); + pt0.setActive(true); + IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); + + pt0 = myPatientDao.read(pt0id); + assertEquals(null, pt0.getMeta().getSource()); + + pt0.getMeta().setSource("urn:source:1"); + pt0.setActive(false); + myPatientDao.update(pt0); + + pt0 = myPatientDao.read(pt0id.withVersion("2")); + assertEquals(null, pt0.getMeta().getSource()); + + // Search without source param + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + IBundleProvider result = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); + + // Search with source param + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Constants.PARAM_SOURCE, new TokenAndListParam() + .addAnd(new TokenParam("urn:source:0"), new TokenParam("@a_request_id"))); + try { + myPatientDao.search(params); + } catch (InvalidRequestException e) { + assertEquals(e.getMessage(), "The _source parameter is disabled on this server"); + } + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + public static void assertConflictException(String theResourceType, ResourceVersionConflictException e) { + assertThat(e.getMessage(), matchesPattern( + "Unable to delete [a-zA-Z]+/[0-9]+ because at least one resource has a reference to this resource. First reference found was resource " + theResourceType + "/[0-9]+ in path [a-zA-Z]+.[a-zA-Z]+")); + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index 980bb11d103..f709df0beb8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -187,7 +187,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { table.setDeleted(new Date()); table = myResourceTableDao.saveAndFlush(table); ResourceHistoryTable newHistory = table.toHistory(); - ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersion(table.getId(), 1L); + ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(table.getId(), 1L); newHistory.setEncoding(currentHistory.getEncoding()); newHistory.setResource(currentHistory.getResource()); myResourceHistoryTableDao.save(newHistory); @@ -2724,7 +2724,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { tx.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - ResourceHistoryTable table = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 1L); + ResourceHistoryTable table = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 1L); String newContent = myFhirCtx.newJsonParser().encodeResourceToString(p); newContent = newContent.replace("male", "foo"); table.setResource(newContent.getBytes(Charsets.UTF_8)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index 2059b333a50..22208bacc05 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -108,20 +108,12 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { return resourceTable.getUpdated().getValueAsString(); }); - myCaptureQueriesListener.clear(); runInTransaction(() -> { Patient p = new Patient(); p.setId(id.getIdPart()); p.addIdentifier().setSystem("urn:system").setValue("2"); - myPatientDao.update(p); + myPatientDao.update(p).getResource(); }); - myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - // TODO: it'd be nice if this was lower - assertEquals(6, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); - myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); - assertThat(myCaptureQueriesListener.getInsertQueriesForCurrentThread(), empty()); - assertThat(myCaptureQueriesListener.getDeleteQueriesForCurrentThread(), empty()); runInTransaction(() -> { List allResources = myResourceTableDao.findAll(); @@ -157,7 +149,9 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { // Do a read { + myCaptureQueriesListener.clear(); Patient patient = myPatientDao.read(id, mySrd); + myCaptureQueriesListener.logAllQueriesForCurrentThread(); List tl = patient.getMeta().getProfile(); assertEquals(1, tl.size()); assertEquals("http://foo/bar", tl.get(0).getValue()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java index 68886499c8f..996c0dd7b32 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java @@ -1,22 +1,23 @@ package ca.uhn.fhir.jpa.dao.r4; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.junit.Assert.assertThat; - -import java.util.List; - +import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.junit.AfterClass; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.util.TestUtil; +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; public class FhirSearchDaoR4Test extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index c90ddea289e..1d2212bf910 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -14,13 +14,9 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.util.TestUtil; -import ca.uhn.fhir.validation.FhirValidator; -import ca.uhn.fhir.validation.ValidationResult; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; -import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle.*; @@ -520,7 +516,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { TransactionTemplate template = new TransactionTemplate(myTxManager); template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); template.execute((TransactionCallback) t -> { - ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), id.getVersionIdPartAsLong()); + ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), id.getVersionIdPartAsLong()); resourceHistoryTable.setEncoding(ResourceEncodingEnum.JSON); try { resourceHistoryTable.setResource("{\"resourceType\":\"FOO\"}".getBytes("UTF-8")); @@ -569,7 +565,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); runInTransaction(()->{ - ResourceHistoryTable historyEntry = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 3); + ResourceHistoryTable historyEntry = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 3); assertNotNull(historyEntry); myResourceHistoryTableDao.delete(historyEntry); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 0a068832f70..abdd8dffb15 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -64,7 +64,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { protected static String ourServerBase; protected static SearchParamRegistryR4 ourSearchParamRegistry; private static DatabaseBackedPagingProvider ourPagingProvider; - protected static ISearchDao mySearchEntityDao; protected static ISearchCoordinatorSvc mySearchCoordinatorSvc; private static GenericWebApplicationContext ourWebApplicationContext; private static SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor; @@ -165,7 +164,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(subsServletHolder.getServlet().getServletConfig().getServletContext()); myValidationSupport = wac.getBean(JpaValidationSupportChainR4.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); - mySearchEntityDao = wac.getBean(ISearchDao.class); ourSearchParamRegistry = wac.getBean(SearchParamRegistryR4.class); ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class); ourSubscriptionMatcherInterceptor.start(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java index 6b98ee2a39b..9bc035957ab 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java @@ -75,6 +75,7 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { Patient p = new Patient(); p.setId("PT-ONEVERSION"); p.getMeta().addTag().setSystem("http://foo").setCode("bar"); + p.getMeta().setSource("http://foo_source"); p.setActive(true); p.addIdentifier().setSystem("foo").setValue("bar"); p.addName().setFamily("FAM"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 5649ddded6b..06b8eca19b9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -19,7 +19,6 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; -import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -3017,7 +3016,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersion(id1.getIdPartAsLong(), 1); + ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id1.getIdPartAsLong(), 1); myResourceHistoryTableDao.delete(version); } }); @@ -3038,15 +3037,18 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { p2.setActive(false); IIdType id2 = ourClient.create().resource(p2).execute().getId(); + myCaptureQueriesListener.clear(); new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersion(id1.getIdPartAsLong(), 1); + ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id1.getIdPartAsLong(), 1); myResourceHistoryTableDao.delete(version); } }); + myCaptureQueriesListener.logAllQueriesForCurrentThread(); Bundle bundle = ourClient.search().forResource("Patient").returnBundle(Bundle.class).execute(); + ourLog.info("Result: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); assertEquals(2, bundle.getTotal()); assertEquals(1, bundle.getEntry().size()); assertEquals(id2.getIdPart(), bundle.getEntry().get(0).getResource().getIdElement().getIdPart()); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index f647d6b84a5..8dae8204771 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask; import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask; import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.util.VersionEnum; import java.util.Arrays; @@ -55,6 +56,29 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init350(); init360(); init400(); + init410(); + } + + protected void init410() { + Builder version = forVersion(VersionEnum.V4_1_0); + + version.startSectionWithMessage("Processing table: HFJ_RES_VER_PROV"); + Builder.BuilderAddTableByColumns resVerProv = version.addTableByColumns("HFJ_RES_VER_PROV", "RES_VER_PID"); + resVerProv.addColumn("RES_VER_PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + resVerProv + .addForeignKey("FK_RESVERPROV_RESVER_PID") + .toColumn("RES_VER_PID") + .references("HFJ_RES_VER", "PID"); + resVerProv.addColumn("RES_PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + resVerProv + .addForeignKey("FK_RESVERPROV_RES_PID") + .toColumn("RES_PID") + .references("HFJ_RESOURCE", "RES_ID"); + resVerProv.addColumn("SOURCE_URI").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, ResourceHistoryProvenanceEntity.SOURCE_URI_LENGTH); + resVerProv.addColumn("REQUEST_ID").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, Constants.REQUEST_ID_LENGTH); + resVerProv.addIndex("IDX_RESVERPROV_SOURCEURI").unique(false).withColumns("SOURCE_URI"); + resVerProv.addIndex("IDX_RESVERPROV_REQUESTID").unique(false).withColumns("REQUEST_ID"); + } protected void init400() { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java new file mode 100644 index 00000000000..19527c96c18 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.jpa.model.entity; + +import ca.uhn.fhir.rest.api.Constants; + +import javax.persistence.*; + +@Table(name = "HFJ_RES_VER_PROV", indexes = { + @Index(name = "IDX_RESVERPROV_SOURCEURI", columnList = "SOURCE_URI"), + @Index(name = "IDX_RESVERPROV_REQUESTID", columnList = "REQUEST_ID") +}) +@Entity +public class ResourceHistoryProvenanceEntity { + + public static final int SOURCE_URI_LENGTH = 100; + + @Id + @Column(name = "RES_VER_PID") + private Long myId; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RES_VER_PID", referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_RESVERPROV_RESVER_PID"), nullable = false, insertable = false, updatable = false) + @MapsId + private ResourceHistoryTable myResourceHistoryTable; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RES_PID", referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_RESVERPROV_RES_PID"), nullable = false) + private ResourceTable myResourceTable; + @Column(name = "SOURCE_URI", length = SOURCE_URI_LENGTH, nullable = true) + private String mySourceUri; + @Column(name = "REQUEST_ID", length = Constants.REQUEST_ID_LENGTH, nullable = true) + private String myRequestId; + + public ResourceTable getResourceTable() { + return myResourceTable; + } + + public void setResourceTable(ResourceTable theResourceTable) { + myResourceTable = theResourceTable; + } + + public ResourceHistoryTable getResourceHistoryTable() { + return myResourceHistoryTable; + } + + public void setResourceHistoryTable(ResourceHistoryTable theResourceHistoryTable) { + myResourceHistoryTable = theResourceHistoryTable; + } + + public String getSourceUri() { + return mySourceUri; + } + + public void setSourceUri(String theSourceUri) { + mySourceUri = theSourceUri; + } + + public String getRequestId() { + return myRequestId; + } + + public void setRequestId(String theRequestId) { + myRequestId = theRequestId; + } + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java index 7411582608a..42c3de0250b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java @@ -20,8 +20,6 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.Constants; import org.hibernate.annotations.OptimisticLock; import javax.persistence.*; @@ -29,7 +27,6 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; -//@formatter:off @Entity @Table(name = "HFJ_RES_VER", uniqueConstraints = { @UniqueConstraint(name = ResourceHistoryTable.IDX_RESVER_ID_VER, columnNames = {"RES_ID", "RES_VER"}) @@ -38,16 +35,14 @@ import java.util.Collection; @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; public static final String IDX_RESVER_ID_VER = "IDX_RESVER_ID_VER"; /** * @see ResourceEncodingEnum */ public static final int ENCODING_COL_LENGTH = 5; - + private static final long serialVersionUID = 1L; @Id @SequenceGenerator(name = "SEQ_RESOURCE_HISTORY_ID", sequenceName = "SEQ_RESOURCE_HISTORY_ID") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESOURCE_HISTORY_ID") @@ -76,10 +71,21 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl @OptimisticLock(excluded = true) private ResourceEncodingEnum myEncoding; + @OneToOne(mappedBy = "myResourceHistoryTable", cascade = {CascadeType.REMOVE}) + private ResourceHistoryProvenanceEntity myProvenance; + public ResourceHistoryTable() { super(); } + public ResourceHistoryProvenanceEntity getProvenance() { + return myProvenance; + } + + public void setProvenance(ResourceHistoryProvenanceEntity theProvenance) { + myProvenance = theProvenance; + } + public void addTag(ResourceHistoryTag theTag) { for (ResourceHistoryTag next : getTags()) { if (next.equals(theTag)) { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index e27ae857298..ff61a5849b0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -214,6 +214,10 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Version @Column(name = "RES_VER") private long myVersion; + @OneToMany(mappedBy = "myResourceTable", fetch = FetchType.LAZY) + private Collection myProvenance; + @Transient + private transient ResourceHistoryTable myCurrentVersionEntity; public Collection getResourceLinksAsTarget() { if (myResourceLinksAsTarget == null) { @@ -610,4 +614,19 @@ public class ResourceTable extends BaseHasResource implements Serializable { } } + /** + * This is a convenience to avoid loading the version a second time within a single transaction. It is + * not persisted. + */ + public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) { + myCurrentVersionEntity = theCurrentVersionEntity; + } + + /** + * This is a convenience to avoid loading the version a second time within a single transaction. It is + * not persisted. + */ + public ResourceHistoryTable getCurrentVersionEntity() { + return myCurrentVersionEntity; + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java index 22dba8eda89..c7301b16f60 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java @@ -32,5 +32,5 @@ public enum TagTypeEnum { PROFILE, SECURITY_LABEL - + } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index e97c14313d5..a85e709bd36 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -228,4 +228,15 @@ public class JpaConstants { */ public static final String EXT_EXTERNALIZED_BINARY_ID = "http://hapifhir.io/fhir/StructureDefinition/externalized-binary-id"; + /** + *

+ * This extension represents the equivalent of the + * Resource.meta.source field within R4+ resources, and is for + * use in DSTU3 resources. It should contain a value of type uri + * and will be located on the Resource.meta + *

+ */ + public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source"; + + } diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java index fa03d66c522..1c8c701b926 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java @@ -1,6 +1,5 @@ package ca.uhn.fhir.jpa.model.entity; -import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.util.TestUtil; import org.junit.AfterClass; import org.junit.Test; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index c43860e4296..7a8b442e753 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -58,7 +58,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; @@ -1124,7 +1123,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer * * + * ${requestId} + * The request ID assigned to this request (either automatically, or via the X-Request-ID header in the request) + * + * * ${operationType} * A code indicating the operation type for this request, e.g. "read", "history-instance", * "extended-operation-instance", etc.) @@ -323,6 +327,8 @@ public class LoggingInterceptor { long time = System.currentTimeMillis() - startTime.getTime(); return Long.toString(time); } + } else if ("requestId".equals(theKey)) { + return myRequestDetails.getRequestId(); } return "!VAL!"; diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java index 6235ee5deba..1834c456688 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java @@ -98,6 +98,7 @@ public abstract class BaseResource extends BaseElement implements IResource { return myContained; } + @Override public IdDt getId() { if (myId == null) { myId = new IdDt(); @@ -121,7 +122,7 @@ public abstract class BaseResource extends BaseElement implements IResource { @Override public IBaseMetaType getMeta() { return new IBaseMetaType() { - + private static final long serialVersionUID = 1L; @Override @@ -132,12 +133,12 @@ public abstract class BaseResource extends BaseElement implements IResource { newTagList.addAll(existingTagList); } ResourceMetadataKeyEnum.PROFILES.put(BaseResource.this, newTagList); - + IdDt tag = new IdDt(theProfile); newTagList.add(tag); return this; } - + @Override public IBaseCoding addSecurity() { List tagList = ResourceMetadataKeyEnum.SECURITY_LABELS.get(BaseResource.this); @@ -149,7 +150,7 @@ public abstract class BaseResource extends BaseElement implements IResource { tagList.add(tag); return tag; } - + @Override public IBaseCoding addTag() { TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(BaseResource.this); @@ -161,17 +162,17 @@ public abstract class BaseResource extends BaseElement implements IResource { tagList.add(tag); return tag; } - + @Override public List getFormatCommentsPost() { return Collections.emptyList(); } - + @Override public List getFormatCommentsPre() { return Collections.emptyList(); } - + @Override public Date getLastUpdated() { InstantDt lu = ResourceMetadataKeyEnum.UPDATED.get(BaseResource.this); @@ -180,7 +181,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return null; } - + @Override public List> getProfile() { ArrayList> retVal = new ArrayList>(); @@ -193,7 +194,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return Collections.unmodifiableList(retVal); } - + @Override public List getSecurity() { ArrayList retVal = new ArrayList(); @@ -206,7 +207,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return Collections.unmodifiableList(retVal); } - + @Override public IBaseCoding getSecurity(String theSystem, String theCode) { for (IBaseCoding next : getSecurity()) { @@ -216,7 +217,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return null; } - + @Override public List getTag() { ArrayList retVal = new ArrayList(); @@ -229,7 +230,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return Collections.unmodifiableList(retVal); } - + @Override public IBaseCoding getTag(String theSystem, String theCode) { for (IBaseCoding next : getTag()) { @@ -239,22 +240,22 @@ public abstract class BaseResource extends BaseElement implements IResource { } return null; } - + @Override public String getVersionId() { return getId().getVersionIdPart(); } - + @Override public boolean hasFormatComment() { return false; } - + @Override public boolean isEmpty() { return getResourceMetadata().isEmpty(); } - + @Override public IBaseMetaType setLastUpdated(Date theHeaderDateValue) { if (theHeaderDateValue == null) { @@ -264,7 +265,7 @@ public abstract class BaseResource extends BaseElement implements IResource { } return this; } - + @Override public IBaseMetaType setVersionId(String theVersionId) { setId(getId().withVersion(theVersionId)); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java index 034f46d31a5..28e5d557339 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java @@ -152,6 +152,26 @@ public class LoggingInterceptorDstu2Test { assertEquals("read - - Patient/1 - ", captor.getValue()); } + @Test + public void testRequestId() throws Exception { + + LoggingInterceptor interceptor = new LoggingInterceptor(); + interceptor.setMessageFormat("${requestId}"); + servlet.getInterceptorService().registerInterceptor(interceptor); + + Logger logger = mock(Logger.class); + interceptor.setLogger(logger); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); + + HttpResponse status = ourClient.execute(httpGet); + IOUtils.closeQuietly(status.getEntity().getContent()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(logger, timeout(1000).times(1)).info(captor.capture()); + assertEquals(Constants.REQUEST_ID_LENGTH, captor.getValue().length()); + } + @Test public void testRequestProcessingTime() throws Exception { diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java index 8c0408fd1b2..5c3e5bc9426 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java @@ -200,7 +200,6 @@ public class InterceptorDstu3Test { when(myInterceptor1.outgoingResponse(nullable(RequestDetails.class), nullable(ResponseDetails.class), nullable(HttpServletRequest.class), nullable(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.outgoingResponse(nullable(RequestDetails.class), nullable(IBaseResource.class), nullable(HttpServletRequest.class), nullable(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.outgoingResponse(nullable(RequestDetails.class), nullable(HttpServletRequest.class), nullable(HttpServletResponse.class))).thenReturn(true); - doThrow(new NullPointerException("FOO")).when(myInterceptor1).processingCompletedNormally(any()); String input = createInput(); diff --git a/pom.xml b/pom.xml index ea8ef24303c..1bb1c3b3ff9 100755 --- a/pom.xml +++ b/pom.xml @@ -594,7 +594,7 @@ 9.4.14.v20181114 3.0.2 - 5.4.2.Final + 5.4.4.Final 5.11.1.Final 5.5.5 @@ -1281,7 +1281,7 @@ org.mockito mockito-core - 2.28.2 + 3.0.0 org.postgresql diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 78e16239557..fc505a69c21 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -7,6 +7,15 @@ + + The version of a few dependencies have been bumped to the + latest versions (dependent HAPI modules listed in brackets): + +
  • Hibernate Core (Core): 5.4.2.Final -> 5.4.4.Final
  • + + ]]> +
    - + + New Feature: + The JPA server now saves and supports searching on Resource.meta.source. The server automatically + appends the Request ID as a hash value on the URI as well in order to provide request level tracking. Searches + can use either the source URI, the request ID, or both. + ]]> + + The email Subscription deliverer now respects the payload property of the subscription when deciding how to encode the resource being sent. Thanks to Sean McIlvenna for the pull request! @@ -543,7 +560,7 @@ not any values contained within. This has been corrected. - The JPA terminology service can now detect when Hibvernate Search (Lucene) + The JPA terminology service can now detect when Hibernate Search (Lucene) is not enabled, and will perform simple ValueSet expansions without relying on Hibenrate Search in such cases.