From 1ba0ae396057cf5b6d42ab0f574db42580bfd52f Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 23 Feb 2016 13:12:30 -0800 Subject: [PATCH] Support inline match URL references, per Simone's requast for the next connectathon --- .../ca/uhn/fhir/i18n/hapi-messages.properties | 3 + .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 1071 +++++++++-------- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 26 + .../uhn/fhir/jpa/dao/data/IForcedIdDao.java | 34 + .../jpa/dao/dstu3/FhirSystemDaoDstu3.java | 2 +- .../java/ca/uhn/fhir/jpa/entity/ForcedId.java | 3 +- .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 104 +- .../uhn/fhirtest/config/TestDstu2Config.java | 1 + .../uhn/fhirtest/config/TestDstu3Config.java | 1 + src/changes/changes.xml | 7 + 10 files changed, 733 insertions(+), 519 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java 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 c8910c7f7b2..8e67b885236 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 @@ -43,6 +43,9 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri # JPA Messages ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request. +ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}" +ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search +ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedNoId=Failed to {0} resource in transaction because no ID was provided ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedUnknownId=Failed to {0} resource in transaction because no resource could be found with ID {1} 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 5224012fed5..63dd5194e3f 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 @@ -82,6 +82,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeChildResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.entity.BaseHasResource; import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; @@ -146,19 +147,27 @@ import ca.uhn.fhir.util.OperationOutcomeUtil; public abstract class BaseHapiFhirDao implements IDao { + public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L); + public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L); + public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; + public static final String OO_SEVERITY_ERROR = "error"; public static final String OO_SEVERITY_INFO = "information"; + public static final String OO_SEVERITY_WARN = "warning"; - /** - * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(Map)} - */ - static final Map> RESOURCE_META_PARAMS; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); + private static final Map ourRetrievalContexts = new HashMap(); /** * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(Map)} */ static final Map>> RESOURCE_META_AND_PARAMS; + /** + * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(Map)} + */ + static final Map> RESOURCE_META_PARAMS; + public static final String UCUM_NS = "http://unitsofmeasure.org"; static { Map> resourceMetaParams = new HashMap>(); Map>> resourceMetaAndParams = new HashMap>>(); @@ -176,14 +185,6 @@ public abstract class BaseHapiFhirDao implements IDao { RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams); } - public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L); - public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L); - public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); - - private static final Map ourRetrievalContexts = new HashMap(); - public static final String UCUM_NS = "http://unitsofmeasure.org"; - @Autowired(required = true) private DaoConfig myConfig; @@ -193,6 +194,9 @@ public abstract class BaseHapiFhirDao implements IDao { @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; + @Autowired + private IForcedIdDao myForcedIdDao; + @Autowired private PlatformTransactionManager myPlatformTransactionManager; @@ -221,23 +225,6 @@ public abstract class BaseHapiFhirDao implements IDao { return InstantDt.withCurrentTime(); } - public void setConfig(DaoConfig theConfig) { - myConfig = theConfig; - } - - public void setEntityManager(EntityManager theEntityManager) { - myEntityManager = theEntityManager; - } - - public void setPlatformTransactionManager(PlatformTransactionManager thePlatformTransactionManager) { - myPlatformTransactionManager = thePlatformTransactionManager; - } - - // @Override - // public void setResourceDaos(List> theResourceDaos) { - // myResourceDaos = theResourceDaos; - // } - protected Set extractResourceLinks(ResourceTable theEntity, IBaseResource theResource) { Set retVal = new HashSet(); @@ -376,6 +363,15 @@ public abstract class BaseHapiFhirDao implements IDao { return retVal; } + protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); + } + + // @Override + // public void setResourceDaos(List> theResourceDaos) { + // myResourceDaos = theResourceDaos; + // } + protected Set extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource); } @@ -384,14 +380,6 @@ public abstract class BaseHapiFhirDao implements IDao { return mySearchParamExtractor.extractSearchParamNumber(theEntity, theResource); } - protected Set extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource); - } - - protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); - } - protected Set extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamQuantity(theEntity, theResource); } @@ -404,6 +392,74 @@ public abstract class BaseHapiFhirDao implements IDao { return mySearchParamExtractor.extractSearchParamTokens(theEntity, theResource); } + protected Set extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource); + } + + private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set allDefs) { + TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); + if (tagList != null) { + for (Tag next : tagList) { + TagDefinition tag = getTag(TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel()); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + + List securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource); + if (securityLabels != null) { + for (BaseCodingDt next : securityLabels) { + TagDefinition tag = getTag(TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue()); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + + List profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource); + if (profiles != null) { + for (IIdType next : profiles) { + TagDefinition tag = getTag(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + } + + private void extractTagsRi(IAnyResource theResource, ResourceTable theEntity, Set allDefs) { + List tagList = theResource.getMeta().getTag(); + if (tagList != null) { + for (IBaseCoding next : tagList) { + TagDefinition tag = getTag(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + + List securityLabels = theResource.getMeta().getSecurity(); + if (securityLabels != null) { + for (IBaseCoding next : securityLabels) { + TagDefinition tag = getTag(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + + List> profiles = theResource.getMeta().getProfile(); + if (profiles != null) { + for (IPrimitiveType next : profiles) { + TagDefinition tag = getTag(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null); + allDefs.add(tag); + theEntity.addTag(tag); + theEntity.setHasTags(true); + } + } + } + private List extractValues(String thePath, IBaseResource theResource) { List values = new ArrayList(); FhirTerser t = getContext().newTerser(); @@ -440,21 +496,6 @@ public abstract class BaseHapiFhirDao implements IDao { } } - protected void notifyInterceptors(RestOperationTypeEnum operationType, ActionRequestDetails requestDetails) { - if (requestDetails.getId() != null && requestDetails.getId().hasResourceType() && isNotBlank(requestDetails.getResourceType())) { - if (requestDetails.getId().getResourceType().equals(requestDetails.getResourceType()) == false) { - throw new InternalErrorException("Inconsistent server state - Resource types don't match: " + requestDetails.getId().getResourceType() + " / " + requestDetails.getResourceType()); - } - } - List interceptors = getConfig().getInterceptors(); - if (interceptors == null) { - return; - } - for (IServerInterceptor next : interceptors) { - next.incomingRequestPreHandled(operationType, requestDetails); - } - } - protected DaoConfig getConfig() { return myConfig; } @@ -594,7 +635,7 @@ public abstract class BaseHapiFhirDao implements IDao { @Override public List getResources(final int theFromIndex, final int theToIndex) { - final StopWatch timer = new StopWatch(); + final StopWatch grTimer = new StopWatch(); TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); return template.execute(new TransactionCallback>() { @Override @@ -603,10 +644,10 @@ public abstract class BaseHapiFhirDao implements IDao { List tupleSubList = tuples.subList(theFromIndex, theToIndex); searchHistoryCurrentVersion(tupleSubList, resEntities); - ourLog.info("Loaded history from current versions in {} ms", timer.getMillisAndRestart()); + ourLog.info("Loaded history from current versions in {} ms", grTimer.getMillisAndRestart()); searchHistoryHistory(tupleSubList, resEntities); - ourLog.info("Loaded history from previous versions in {} ms", timer.getMillisAndRestart()); + ourLog.info("Loaded history from previous versions in {} ms", grTimer.getMillisAndRestart()); Collections.sort(resEntities, new Comparator() { @Override @@ -615,9 +656,9 @@ public abstract class BaseHapiFhirDao implements IDao { } }); - int limit = theToIndex - theFromIndex; - if (resEntities.size() > limit) { - resEntities = resEntities.subList(0, limit); + int grLimit = theToIndex - theFromIndex; + if (resEntities.size() > grLimit) { + resEntities = resEntities.subList(0, grLimit); } ArrayList retVal = new ArrayList(); @@ -632,7 +673,7 @@ public abstract class BaseHapiFhirDao implements IDao { } throw e; } - IBaseResource resource = (IBaseResource) toResource(type.getImplementingClass(), next, true); + IBaseResource resource = toResource(type.getImplementingClass(), next, true); retVal.add(resource); } return retVal; @@ -641,29 +682,55 @@ public abstract class BaseHapiFhirDao implements IDao { } @Override - public int size() { - return tuples.size(); + public Integer preferredPageSize() { + return null; } @Override - public Integer preferredPageSize() { - return null; + public int size() { + return tuples.size(); } }; } - protected static boolean isValidPid(IIdType theId) { - if (theId == null || theId.getIdPart() == null) { - return false; - } - String idPart = theId.getIdPart(); - for (int i = 0; i < idPart.length(); i++) { - char nextChar = idPart.charAt(i); - if (nextChar < '0' || nextChar > '9') { - return false; + protected void notifyInterceptors(RestOperationTypeEnum operationType, ActionRequestDetails requestDetails) { + if (requestDetails.getId() != null && requestDetails.getId().hasResourceType() && isNotBlank(requestDetails.getResourceType())) { + if (requestDetails.getId().getResourceType().equals(requestDetails.getResourceType()) == false) { + throw new InternalErrorException("Inconsistent server state - Resource types don't match: " + requestDetails.getId().getResourceType() + " / " + requestDetails.getResourceType()); } } - return true; + List interceptors = getConfig().getInterceptors(); + if (interceptors == null) { + return; + } + for (IServerInterceptor next : interceptors) { + next.incomingRequestPreHandled(operationType, requestDetails); + } + } + + private String parseContentTextIntoWords(IBaseResource theResource) { + StringBuilder retVal = new StringBuilder(); + @SuppressWarnings("rawtypes") + List childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class); + for (@SuppressWarnings("rawtypes") + IPrimitiveType nextType : childElements) { + if (nextType instanceof StringDt || nextType.getClass().equals(StringType.class)) { + String nextValue = nextType.getValueAsString(); + if (isNotBlank(nextValue)) { + retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); + retVal.append("\n"); + } + } + } + return retVal.toString(); + } + + private void populateResourceId(final IBaseResource theResource, BaseHasResource theEntity) { + IIdType id = theEntity.getIdDt(); + if (getContext().getVersion().getVersion().isRi()) { + id = new IdType(id.getValue()); + } + theResource.setId(id); } protected void populateResourceIntoEntity(IBaseResource theResource, ResourceTable theEntity) { @@ -728,115 +795,159 @@ public abstract class BaseHapiFhirDao implements IDao { } - private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set allDefs) { - TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); - if (tagList != null) { - for (Tag next : tagList) { - TagDefinition tag = getTag(TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel()); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); + private R populateResourceMetadataHapi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IResource res) { + R retVal = (R) res; + if (theEntity.getDeleted() != null) { + res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance(); + retVal = (R) res; + ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); + if (theForHistoryOperation) { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE); + } + } else if (theForHistoryOperation) { + /* + * If the create and update times match, this was when the resource was created so we should mark it as a POST. + * Otherwise, it's a PUT. + */ + Date published = theEntity.getPublished().getValue(); + Date updated = theEntity.getUpdated().getValue(); + if (published.equals(updated)) { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST); + } else { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT); } } - List securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource); - if (securityLabels != null) { - for (BaseCodingDt next : securityLabels) { - TagDefinition tag = getTag(TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue()); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); - } + res.setId(theEntity.getIdDt()); + + ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); + ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished()); + ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); + IDao.RESOURCE_PID.put(res, theEntity.getId()); + + if (theEntity.getTitle() != null) { + ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle()); } - List profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource); - if (profiles != null) { - for (IIdType next : profiles) { - TagDefinition tag = getTag(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); + Collection tags = theEntity.getTags(); + if (theEntity.isHasTags()) { + TagList tagList = new TagList(); + List securityLabels = new ArrayList(); + List profiles = new ArrayList(); + for (BaseTag next : tags) { + switch (next.getTag().getTagType()) { + case PROFILE: + profiles.add(new IdDt(next.getTag().getCode())); + break; + case SECURITY_LABEL: + IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt(); + secLabel.setSystem(next.getTag().getSystem()); + secLabel.setCode(next.getTag().getCode()); + secLabel.setDisplay(next.getTag().getDisplay()); + securityLabels.add(secLabel); + break; + case TAG: + tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay())); + break; + } + } + if (tagList.size() > 0) { + ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList); + } + if (securityLabels.size() > 0) { + ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels)); + } + if (profiles.size() > 0) { + ResourceMetadataKeyEnum.PROFILES.put(res, profiles); } } + + return retVal; } - private void extractTagsRi(IAnyResource theResource, ResourceTable theEntity, Set allDefs) { - List tagList = theResource.getMeta().getTag(); - if (tagList != null) { - for (IBaseCoding next : tagList) { - TagDefinition tag = getTag(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); + private R populateResourceMetadataRi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IAnyResource res) { + R retVal = (R) res; + if (theEntity.getDeleted() != null) { + res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance(); + retVal = (R) res; + ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); + if (theForHistoryOperation) { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode()); + } + } else if (theForHistoryOperation) { + /* + * If the create and update times match, this was when the resource was created so we should mark it as a POST. + * Otherwise, it's a PUT. + */ + Date published = theEntity.getPublished().getValue(); + Date updated = theEntity.getUpdated().getValue(); + if (published.equals(updated)) { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode()); + } else { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode()); } } - List securityLabels = theResource.getMeta().getSecurity(); - if (securityLabels != null) { - for (IBaseCoding next : securityLabels) { - TagDefinition tag = getTag(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); - } - } + res.getMeta().getTag().clear(); + res.getMeta().getProfile().clear(); + res.getMeta().getSecurity().clear(); + res.getMeta().setLastUpdated(null); + res.getMeta().setVersionId(null); + + populateResourceId(res, theEntity); - List> profiles = theResource.getMeta().getProfile(); - if (profiles != null) { - for (IPrimitiveType next : profiles) { - TagDefinition tag = getTag(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null); - allDefs.add(tag); - theEntity.addTag(tag); - theEntity.setHasTags(true); + res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); + IDao.RESOURCE_PID.put(res, theEntity.getId()); + + Collection tags = theEntity.getTags(); + if (theEntity.isHasTags()) { + for (BaseTag next : tags) { + switch (next.getTag().getTagType()) { + case PROFILE: + res.getMeta().addProfile(next.getTag().getCode()); + break; + case SECURITY_LABEL: + IBaseCoding sec = res.getMeta().addSecurity(); + sec.setSystem(next.getTag().getSystem()); + sec.setCode(next.getTag().getCode()); + sec.setDisplay(next.getTag().getDisplay()); + break; + case TAG: + IBaseCoding tag = res.getMeta().addTag(); + tag.setSystem(next.getTag().getSystem()); + tag.setCode(next.getTag().getCode()); + tag.setDisplay(next.getTag().getDisplay()); + break; + } } } + return retVal; } /** - * This method is called when an update to an existing resource detects that the resource supplied for update is - * missing a tag/profile/security label that the currently persisted resource holds. - *

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

+ * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the + * first time. * * @param theEntity - * The entity being updated (Do not modify the entity! Undefined behaviour will occur!) - * @param theTag - * The tag - * @return Retturns true if the tag should be removed - * @see Updates to Tags, Profiles, and Security - * Labels for a description of the logic that the default behaviour folows. + * The resource + * @param theResource + * The resource being persisted */ - protected boolean shouldDroppedTagBeRemovedOnUpdate(ResourceTable theEntity, ResourceTag theTag) { - if (theTag.getTag().getTagType() == TagTypeEnum.PROFILE) { - return true; - } - return false; + protected void postPersist(ResourceTable theEntity, T theResource) { + // nothing } - @CoverageIgnore - protected static IQueryParameterAnd newInstanceAnd(String chain) { - IQueryParameterAnd type; - Class> clazz = RESOURCE_META_AND_PARAMS.get(chain); - try { - type = clazz.newInstance(); - } catch (Exception e) { - throw new InternalErrorException("Failure creating instance of " + clazz, e); - } - return type; - } - - @CoverageIgnore - protected static IQueryParameterType newInstanceType(String chain) { - IQueryParameterType type; - Class clazz = RESOURCE_META_PARAMS.get(chain); - try { - type = clazz.newInstance(); - } catch (Exception e) { - throw new InternalErrorException("Failure creating instance of " + clazz, e); - } - return type; + /** + * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the + * first time. + * + * @param theEntity + * The resource + * @param theResource + * The resource being persisted + */ + protected void postUpdate(ResourceTable theEntity, T theResource) { + // nothing } protected Set processMatchUrl(String theMatchUrl, Class theResourceType) { @@ -854,100 +965,8 @@ public abstract class BaseHapiFhirDao implements IDao { return ids; } - public static SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition resourceDef) { - SearchParameterMap paramMap = new SearchParameterMap(); - List parameters = translateMatchUrl(theMatchUrl); - - ArrayListMultimap nameToParamLists = ArrayListMultimap.create(); - for (NameValuePair next : parameters) { - if (isBlank(next.getValue())) { - continue; - } - - String paramName = next.getName(); - String qualifier = null; - for (int i = 0; i < paramName.length(); i++) { - switch (paramName.charAt(i)) { - case '.': - case ':': - qualifier = paramName.substring(i); - paramName = paramName.substring(0, i); - i = Integer.MAX_VALUE - 1; - break; - } - } - - QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue()); - nameToParamLists.put(paramName, paramList); - } - - for (String nextParamName : nameToParamLists.keySet()) { - List paramList = nameToParamLists.get(nextParamName); - if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { - if (paramList != null && paramList.size() > 0) { - if (paramList.size() > 2) { - throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions"); - } else { - DateRangeParam p1 = new DateRangeParam(); - p1.setValuesAsQueryTokens(paramList); - paramMap.setLastUpdated(p1); - } - } - continue; - } - - if (Constants.PARAM_COUNT.equals(nextParamName)) { - if (paramList.size() > 0 && paramList.get(0).size() > 0) { - String intString = paramList.get(0).get(0); - try { - paramMap.setCount(Integer.parseInt(intString)); - } catch (NumberFormatException e) { - throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString); - } - } - continue; - } - - if (RESOURCE_META_PARAMS.containsKey(nextParamName)) { - if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) { - throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); - } - IQueryParameterAnd type = newInstanceAnd(nextParamName); - type.setValuesAsQueryTokens((paramList)); - paramMap.add(nextParamName, type); - } else if (nextParamName.startsWith("_")) { - // ignore these since they aren't search params (e.g. _sort) - } else { - RuntimeSearchParam paramDef = resourceDef.getSearchParam(nextParamName); - if (paramDef == null) { - throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); - } - - IQueryParameterAnd param = MethodUtil.parseQueryParams(paramDef, nextParamName, paramList); - paramMap.add(nextParamName, param); - } - } - return paramMap; - } - - protected static List translateMatchUrl(String theMatchUrl) { - List parameters; - String matchUrl = theMatchUrl; - int questionMarkIndex = matchUrl.indexOf('?'); - if (questionMarkIndex != -1) { - matchUrl = matchUrl.substring(questionMarkIndex + 1); - } - matchUrl = matchUrl.replace("|", "%7C"); - matchUrl = matchUrl.replace("=>=", "=%3E%3D"); - matchUrl = matchUrl.replace("=<=", "=%3C%3D"); - matchUrl = matchUrl.replace("=>", "=%3E"); - matchUrl = matchUrl.replace("=<", "=%3C"); - if (matchUrl.contains(" ")) { - throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)"); - } - - parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&'); - return parameters; + public BaseHasResource readEntity(IIdType theValueId) { + throw new NotImplementedException(""); } private void searchHistoryCurrentVersion(List theTuples, List theRetVal) { @@ -1086,6 +1105,10 @@ public abstract class BaseHapiFhirDao implements IDao { } } + public void setConfig(DaoConfig theConfig) { + myConfig = theConfig; + } + // protected MetaDt toMetaDt(Collection tagDefinitions) { // MetaDt retVal = new MetaDt(); // for (TagDefinition next : tagDefinitions) { @@ -1122,6 +1145,37 @@ public abstract class BaseHapiFhirDao implements IDao { } } + public void setEntityManager(EntityManager theEntityManager) { + myEntityManager = theEntityManager; + } + + public void setPlatformTransactionManager(PlatformTransactionManager thePlatformTransactionManager) { + myPlatformTransactionManager = thePlatformTransactionManager; + } + + /** + * This method is called when an update to an existing resource detects that the resource supplied for update is + * missing a tag/profile/security label that the currently persisted resource holds. + *

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

+ * + * @param theEntity + * The entity being updated (Do not modify the entity! Undefined behaviour will occur!) + * @param theTag + * The tag + * @return Retturns true if the tag should be removed + * @see Updates to Tags, Profiles, and Security + * Labels for a description of the logic that the default behaviour folows. + */ + protected boolean shouldDroppedTagBeRemovedOnUpdate(ResourceTable theEntity, ResourceTag theTag) { + if (theTag.getTag().getTagType() == TagTypeEnum.PROFILE) { + return true; + } + return false; + } + protected ResourceTable toEntity(IResource theResource) { ResourceTable retVal = new ResourceTable(); @@ -1136,191 +1190,54 @@ public abstract class BaseHapiFhirDao implements IDao { return toResource(type.getImplementingClass(), theEntity, theForHistoryOperation); } - @Override - @SuppressWarnings("unchecked") - public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { - String resourceText = null; - switch (theEntity.getEncoding()) { - case JSON: + @Override + @SuppressWarnings("unchecked") + public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { + String resourceText = null; + switch (theEntity.getEncoding()) { + case JSON: + try { + resourceText = new String(theEntity.getResource(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Should not happen", e); + } + break; + case JSONC: + resourceText = GZipUtil.decompress(theEntity.getResource()); + break; + } + + IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); + R retVal; try { - resourceText = new String(theEntity.getResource(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new Error("Should not happen", e); + retVal = parser.parseResource(theResourceType, resourceText); + } catch (Exception e) { + StringBuilder b = new StringBuilder(); + b.append("Failed to parse database resource["); + b.append(theResourceType); + b.append("/"); + b.append(theEntity.getIdDt().getIdPart()); + b.append(" (pid "); + b.append(theEntity.getId()); + b.append(", version "); + b.append(myContext.getVersion().getVersion()); + b.append("): "); + b.append(e.getMessage()); + String msg = b.toString(); + ourLog.error(msg, e); + throw new DataFormatException(msg, e); } - break; - case JSONC: - resourceText = GZipUtil.decompress(theEntity.getResource()); - break; - } - IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); - R retVal; - try { - retVal = parser.parseResource(theResourceType, resourceText); - } catch (Exception e) { - StringBuilder b = new StringBuilder(); - b.append("Failed to parse database resource["); - b.append(theResourceType); - b.append("/"); - b.append(theEntity.getIdDt().getIdPart()); - b.append(" (pid "); - b.append(theEntity.getId()); - b.append(", version "); - b.append(myContext.getVersion().getVersion()); - b.append("): "); - b.append(e.getMessage()); - String msg = b.toString(); - ourLog.error(msg, e); - throw new DataFormatException(msg, e); - } - - if (retVal instanceof IResource) { - IResource res = (IResource) retVal; - retVal = populateResourceMetadataHapi(theResourceType, theEntity, theForHistoryOperation, res); - } else { - IAnyResource res = (IAnyResource) retVal; - retVal = populateResourceMetadataRi(theResourceType, theEntity, theForHistoryOperation, res); - } - return retVal; - } - - private R populateResourceMetadataHapi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IResource res) { - R retVal = (R) res; - if (theEntity.getDeleted() != null) { - res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance(); - retVal = (R) res; - ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); - if (theForHistoryOperation) { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE); - } - } else if (theForHistoryOperation) { - /* - * If the create and update times match, this was when the resource was created so we should mark it as a POST. - * Otherwise, it's a PUT. - */ - Date published = theEntity.getPublished().getValue(); - Date updated = theEntity.getUpdated().getValue(); - if (published.equals(updated)) { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST); + if (retVal instanceof IResource) { + IResource res = (IResource) retVal; + retVal = populateResourceMetadataHapi(theResourceType, theEntity, theForHistoryOperation, res); } else { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT); + IAnyResource res = (IAnyResource) retVal; + retVal = populateResourceMetadataRi(theResourceType, theEntity, theForHistoryOperation, res); } + return retVal; } - res.setId(theEntity.getIdDt()); - - ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); - ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished()); - ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); - IDao.RESOURCE_PID.put(res, theEntity.getId()); - - if (theEntity.getTitle() != null) { - ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle()); - } - - Collection tags = theEntity.getTags(); - if (theEntity.isHasTags()) { - TagList tagList = new TagList(); - List securityLabels = new ArrayList(); - List profiles = new ArrayList(); - for (BaseTag next : tags) { - switch (next.getTag().getTagType()) { - case PROFILE: - profiles.add(new IdDt(next.getTag().getCode())); - break; - case SECURITY_LABEL: - IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt(); - secLabel.setSystem(next.getTag().getSystem()); - secLabel.setCode(next.getTag().getCode()); - secLabel.setDisplay(next.getTag().getDisplay()); - securityLabels.add(secLabel); - break; - case TAG: - tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay())); - break; - } - } - if (tagList.size() > 0) { - ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList); - } - if (securityLabels.size() > 0) { - ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels)); - } - if (profiles.size() > 0) { - ResourceMetadataKeyEnum.PROFILES.put(res, profiles); - } - } - - return retVal; - } - - private R populateResourceMetadataRi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IAnyResource res) { - R retVal = (R) res; - if (theEntity.getDeleted() != null) { - res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance(); - retVal = (R) res; - ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); - if (theForHistoryOperation) { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode()); - } - } else if (theForHistoryOperation) { - /* - * If the create and update times match, this was when the resource was created so we should mark it as a POST. - * Otherwise, it's a PUT. - */ - Date published = theEntity.getPublished().getValue(); - Date updated = theEntity.getUpdated().getValue(); - if (published.equals(updated)) { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode()); - } else { - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode()); - } - } - - res.getMeta().getTag().clear(); - res.getMeta().getProfile().clear(); - res.getMeta().getSecurity().clear(); - res.getMeta().setLastUpdated(null); - res.getMeta().setVersionId(null); - - populateResourceId(res, theEntity); - - res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); - IDao.RESOURCE_PID.put(res, theEntity.getId()); - - Collection tags = theEntity.getTags(); - if (theEntity.isHasTags()) { - for (BaseTag next : tags) { - switch (next.getTag().getTagType()) { - case PROFILE: - res.getMeta().addProfile(next.getTag().getCode()); - break; - case SECURITY_LABEL: - IBaseCoding sec = res.getMeta().addSecurity(); - sec.setSystem(next.getTag().getSystem()); - sec.setCode(next.getTag().getCode()); - sec.setDisplay(next.getTag().getDisplay()); - break; - case TAG: - IBaseCoding tag = res.getMeta().addTag(); - tag.setSystem(next.getTag().getSystem()); - tag.setCode(next.getTag().getCode()); - tag.setDisplay(next.getTag().getDisplay()); - break; - } - } - } - return retVal; - } - - private static List toBaseCodingList(List theSecurityLabels) { - ArrayList retVal = new ArrayList(theSecurityLabels.size()); - for (IBaseCoding next : theSecurityLabels) { - retVal.add((BaseCodingDt) next); - } - return retVal; - } - protected String toResourceName(Class theResourceType) { return myContext.getResourceDefinition(theResourceType).getName(); } @@ -1333,28 +1250,8 @@ public abstract class BaseHapiFhirDao implements IDao { return translateForcedIdToPid(theId, myEntityManager); } - public static void validateResourceType(BaseHasResource theEntity, String theResourceName) { - if (!theResourceName.equals(theEntity.getResourceType())) { - throw new ResourceNotFoundException("Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); - } - } - - static Long translateForcedIdToPid(IIdType theId, EntityManager entityManager) { - if (isValidPid(theId)) { - return theId.getIdPartAsLong(); - } else { - TypedQuery q = entityManager.createNamedQuery("Q_GET_FORCED_ID", ForcedId.class); - q.setParameter("ID", theId.getIdPart()); - try { - return q.getSingleResult().getResourcePid(); - } catch (NoResultException e) { - throw new ResourceNotFoundException(theId); - } - } - } - protected String translatePidIdToForcedId(Long theId) { - ForcedId forcedId = myEntityManager.find(ForcedId.class, theId); + ForcedId forcedId = myForcedIdDao.findByResourcePid(theId); if (forcedId != null) { return forcedId.getForcedId(); } else { @@ -1362,10 +1259,6 @@ public abstract class BaseHapiFhirDao implements IDao { } } - protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, Date theUpdateTime) { - return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true, theUpdateTime); - } - @SuppressWarnings("unchecked") protected ResourceTable updateEntity(final IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime) { @@ -1477,6 +1370,49 @@ public abstract class BaseHapiFhirDao implements IDao { } } + /* + * Handle references within the resource that are match URLs, for example + * references like "Patient?identifier=foo". These match URLs are resolved + * and replaced with the ID of the matching resource. + */ + if (myConfig.isAllowInlineMatchUrlReferences()) { + FhirTerser terser = getContext().newTerser(); + List allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); + for (IBaseReference nextRef : allRefs) { + IIdType nextId = nextRef.getReferenceElement(); + String nextIdText = nextId.getValue(); + int qmIndex = nextIdText.indexOf('?'); + if (qmIndex != -1) { + for (int i = qmIndex - 1; i >= 0; i--) { + if (nextIdText.charAt(i) == '/') { + nextIdText = nextIdText.substring(i + 1); + break; + } + } + String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')); + RuntimeResourceDefinition matchResourceDef = getContext().getResourceDefinition(resourceTypeString); + if (matchResourceDef == null) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString); + throw new InvalidRequestException(msg); + } + Class matchResourceType = matchResourceDef.getImplementingClass(); + Set matches = processMatchUrl(nextIdText, matchResourceType); + if (matches.isEmpty()) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue()); + throw new InvalidRequestException(msg); + } + if (matches.size() > 1) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue()); + throw new InvalidRequestException(msg); + } + Long next = matches.iterator().next(); + String newId = resourceTypeString + '/' + translatePidIdToForcedId(next); + ourLog.info("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId); + nextRef.setReference(newId); + } + } + } + links = extractResourceLinks(theEntity, theResource); /* @@ -1620,40 +1556,25 @@ public abstract class BaseHapiFhirDao implements IDao { return theEntity; } - private void populateResourceId(final IBaseResource theResource, BaseHasResource theEntity) { - IIdType id = theEntity.getIdDt(); - if (getContext().getVersion().getVersion().isRi()) { - id = new IdType(id.getValue()); + protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, Date theUpdateTime) { + return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true, theUpdateTime); + } + + protected void validateDeleteConflictsEmptyOrThrowException(List theDeleteConflicts) { + if (theDeleteConflicts.isEmpty()) { + return; } - theResource.setId(id); - } - /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the - * first time. - * - * @param theEntity - * The resource - * @param theResource - * The resource being persisted - */ - protected void postUpdate(ResourceTable theEntity, T theResource) { - // nothing - } + IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); + for (DeleteConflict next : theDeleteConflicts) { + String msg = "Unable to delete " + next.getTargetId().toUnqualifiedVersionless().getValue() + " because at least one resource has a reference to this resource. First reference found was resource " + next.getTargetId().toUnqualifiedVersionless().getValue() + " in path " + + next.getSourcePath(); + OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing"); + } - /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the - * first time. - * - * @param theEntity - * The resource - * @param theResource - * The resource being persisted - */ - protected void postPersist(ResourceTable theEntity, T theResource) { - // nothing + throw new ResourceVersionConflictException("Delete failed because of constraint failure", oo); } - + /** * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow * the DAO to ensure that it is valid for persistence. By default, checks for the "subsetted" tag and rejects @@ -1682,6 +1603,44 @@ public abstract class BaseHapiFhirDao implements IDao { } } + protected static boolean isValidPid(IIdType theId) { + if (theId == null || theId.getIdPart() == null) { + return false; + } + String idPart = theId.getIdPart(); + for (int i = 0; i < idPart.length(); i++) { + char nextChar = idPart.charAt(i); + if (nextChar < '0' || nextChar > '9') { + return false; + } + } + return true; + } + + @CoverageIgnore + protected static IQueryParameterAnd newInstanceAnd(String chain) { + IQueryParameterAnd type; + Class> clazz = RESOURCE_META_AND_PARAMS.get(chain); + try { + type = clazz.newInstance(); + } catch (Exception e) { + throw new InternalErrorException("Failure creating instance of " + clazz, e); + } + return type; + } + + @CoverageIgnore + protected static IQueryParameterType newInstanceType(String chain) { + IQueryParameterType type; + Class clazz = RESOURCE_META_PARAMS.get(chain); + try { + type = clazz.newInstance(); + } catch (Exception e) { + throw new InternalErrorException("Failure creating instance of " + clazz, e); + } + return type; + } + public static String normalizeString(String theString) { char[] out = new char[theString.length()]; theString = Normalizer.normalize(theString, Normalizer.Form.NFD); @@ -1731,40 +1690,128 @@ public abstract class BaseHapiFhirDao implements IDao { return b.toString(); } - private String parseContentTextIntoWords(IBaseResource theResource) { - StringBuilder retVal = new StringBuilder(); - @SuppressWarnings("rawtypes") - List childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class); - for (@SuppressWarnings("rawtypes") - IPrimitiveType nextType : childElements) { - if (nextType instanceof StringDt || nextType.getClass().equals(StringType.class)) { - String nextValue = nextType.getValueAsString(); - if (isNotBlank(nextValue)) { - retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); - retVal.append("\n"); - } + private static List toBaseCodingList(List theSecurityLabels) { + ArrayList retVal = new ArrayList(theSecurityLabels.size()); + for (IBaseCoding next : theSecurityLabels) { + retVal.add((BaseCodingDt) next); + } + return retVal; +} + + static Long translateForcedIdToPid(IIdType theId, EntityManager entityManager) { + if (isValidPid(theId)) { + return theId.getIdPartAsLong(); + } else { + TypedQuery q = entityManager.createNamedQuery("Q_GET_FORCED_ID", ForcedId.class); + q.setParameter("ID", theId.getIdPart()); + try { + return q.getSingleResult().getResourcePid(); + } catch (NoResultException e) { + throw new ResourceNotFoundException(theId); } } - return retVal.toString(); } - protected void validateDeleteConflictsEmptyOrThrowException(List theDeleteConflicts) { - if (theDeleteConflicts.isEmpty()) { - return; + protected static List translateMatchUrl(String theMatchUrl) { + List parameters; + String matchUrl = theMatchUrl; + int questionMarkIndex = matchUrl.indexOf('?'); + if (questionMarkIndex != -1) { + matchUrl = matchUrl.substring(questionMarkIndex + 1); + } + matchUrl = matchUrl.replace("|", "%7C"); + matchUrl = matchUrl.replace("=>=", "=%3E%3D"); + matchUrl = matchUrl.replace("=<=", "=%3C%3D"); + matchUrl = matchUrl.replace("=>", "=%3E"); + matchUrl = matchUrl.replace("=<", "=%3C"); + if (matchUrl.contains(" ")) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)"); } - IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); - for (DeleteConflict next : theDeleteConflicts) { - String msg = "Unable to delete " + next.getTargetId().toUnqualifiedVersionless().getValue() + " because at least one resource has a reference to this resource. First reference found was resource " + next.getTargetId().toUnqualifiedVersionless().getValue() + " in path " - + next.getSourcePath(); - OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing"); - } - - throw new ResourceVersionConflictException("Delete failed because of constraint failure", oo); + parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&'); + return parameters; } - public BaseHasResource readEntity(IIdType theValueId) { - throw new NotImplementedException(""); + public static SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition resourceDef) { + SearchParameterMap paramMap = new SearchParameterMap(); + List parameters = translateMatchUrl(theMatchUrl); + + ArrayListMultimap nameToParamLists = ArrayListMultimap.create(); + for (NameValuePair next : parameters) { + if (isBlank(next.getValue())) { + continue; + } + + String paramName = next.getName(); + String qualifier = null; + for (int i = 0; i < paramName.length(); i++) { + switch (paramName.charAt(i)) { + case '.': + case ':': + qualifier = paramName.substring(i); + paramName = paramName.substring(0, i); + i = Integer.MAX_VALUE - 1; + break; + } + } + + QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue()); + nameToParamLists.put(paramName, paramList); + } + + for (String nextParamName : nameToParamLists.keySet()) { + List paramList = nameToParamLists.get(nextParamName); + if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { + if (paramList != null && paramList.size() > 0) { + if (paramList.size() > 2) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions"); + } else { + DateRangeParam p1 = new DateRangeParam(); + p1.setValuesAsQueryTokens(paramList); + paramMap.setLastUpdated(p1); + } + } + continue; + } + + if (Constants.PARAM_COUNT.equals(nextParamName)) { + if (paramList.size() > 0 && paramList.get(0).size() > 0) { + String intString = paramList.get(0).get(0); + try { + paramMap.setCount(Integer.parseInt(intString)); + } catch (NumberFormatException e) { + throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString); + } + } + continue; + } + + if (RESOURCE_META_PARAMS.containsKey(nextParamName)) { + if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) { + throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); + } + IQueryParameterAnd type = newInstanceAnd(nextParamName); + type.setValuesAsQueryTokens((paramList)); + paramMap.add(nextParamName, type); + } else if (nextParamName.startsWith("_")) { + // ignore these since they aren't search params (e.g. _sort) + } else { + RuntimeSearchParam paramDef = resourceDef.getSearchParam(nextParamName); + if (paramDef == null) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); + } + + IQueryParameterAnd param = MethodUtil.parseQueryParams(paramDef, nextParamName, paramList); + paramMap.add(nextParamName, param); + } + } + return paramMap; + } + + public static void validateResourceType(BaseHasResource theEntity, String theResourceName) { + if (!theResourceName.equals(theEntity.getResourceType())) { + throw new ResourceNotFoundException("Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); + } } } 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 1bec9df1921..bc55967eb8d 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 @@ -34,6 +34,32 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; public class DaoConfig { + // *** + // update setter javadoc if default changes + // *** + private boolean myAllowInlineMatchUrlReferences = false; + + /** + * @see #setAllowInlineMatchUrlReferences(boolean) + */ + public boolean isAllowInlineMatchUrlReferences() { + return myAllowInlineMatchUrlReferences; + } + + /** + * Should references containing match URLs be resolved and replaced in create and update operations. For + * example, if this property is set to true and a resource is created containing a reference + * to "Patient?identifier=12345", this is reference match URL will be resolved and replaced according + * to the usual match URL rules. + *

+ * Default is false for now, as this is an experimental feature. + *

+ * @since 1.5 + */ + public void setAllowInlineMatchUrlReferences(boolean theAllowInlineMatchUrlReferences) { + myAllowInlineMatchUrlReferences = theAllowInlineMatchUrlReferences; + } + private boolean myAllowMultipleDelete; private int myHardSearchLimit = 1000; private int myHardTagListLimit = 1000; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java new file mode 100644 index 00000000000..5abd3907349 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.dao.data; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2016 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import ca.uhn.fhir.jpa.entity.ForcedId; + +public interface IForcedIdDao extends JpaRepository { + + @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid") + public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java index b057355237d..3e7095f6444 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java @@ -462,7 +462,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { FhirTerser terser = getContext().newTerser(); for (DaoMethodOutcome nextOutcome : idToPersistedOutcome.values()) { - IBaseResource nextResource = (IBaseResource) nextOutcome.getResource(); + IBaseResource nextResource = nextOutcome.getResource(); if (nextResource == null) { continue; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java index 2653f61afb1..e21fb56ef32 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java @@ -35,7 +35,8 @@ import javax.persistence.UniqueConstraint; //@formatter:off @Entity() @Table(name = "HFJ_FORCED_ID", uniqueConstraints = { - @UniqueConstraint(name = "IDX_FORCEDID", columnNames = {"FORCED_ID"}) + @UniqueConstraint(name = "IDX_FORCEDID", columnNames = {"FORCED_ID"}), + @UniqueConstraint(name = "IDX_FORCEDID_RESID", columnNames = {"RESOURCE_PID"}) }) @NamedQueries(value = { @NamedQuery(name = "Q_GET_FORCED_ID", query = "SELECT f FROM ForcedId f WHERE myForcedId = :ID") diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index f11c2444729..461a3f0d23f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -17,10 +17,8 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; @@ -44,8 +42,10 @@ import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; @@ -113,7 +113,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { // Try making the resource unparseable TransactionTemplate template = new TransactionTemplate(myTxManager); - template.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); template.execute(new TransactionCallback() { @Override public ResourceTable doInTransaction(TransactionStatus theStatus) { @@ -300,12 +300,106 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1")); assertEquals("1", respEntry.getResponse().getEtag()); - o = (Observation) myObservationDao.read(new IdType(respEntry.getResponse().getLocationElement())); + o = myObservationDao.read(new IdType(respEntry.getResponse().getLocationElement())); assertEquals(id.toVersionless().getValue(), o.getSubject().getReference()); assertEquals("1", o.getIdElement().getVersionIdPart()); } + @After + public void after() { + myDaoConfig.setAllowInlineMatchUrlReferences(false); + } + + @Test + public void testTransactionCreateInlineMatchUrlWithOneMatch() { + String methodName = "testTransactionCreateInlineMatchUrlWithOneMatch"; + Bundle request = new Bundle(); + + myDaoConfig.setAllowInlineMatchUrlReferences(true); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.setId("Patient/" + methodName); + IIdType id = myPatientDao.update(p).getId(); + ourLog.info("Created patient, got it: {}", id); + + Observation o = new Observation(); + o.getCode().setText("Some Observation"); + o.getSubject().setReference("Patient?identifier=urn%3Asystem%7C" + methodName); + request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST); + + Bundle resp = mySystemDao.transaction(myRequestDetails, request); + assertEquals(1, resp.getEntry().size()); + + BundleEntryComponent respEntry = resp.getEntry().get(0); + assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus()); + assertThat(respEntry.getResponse().getLocation(), containsString("Observation/")); + assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1")); + assertEquals("1", respEntry.getResponse().getEtag()); + + o = myObservationDao.read(new IdType(respEntry.getResponse().getLocationElement())); + assertEquals(id.toVersionless().getValue(), o.getSubject().getReference()); + assertEquals("1", o.getIdElement().getVersionIdPart()); + + } + + @Test + public void testTransactionCreateInlineMatchUrlWithNoMatches() { + String methodName = "testTransactionCreateInlineMatchUrlWithNoMatches"; + Bundle request = new Bundle(); + + myDaoConfig.setAllowInlineMatchUrlReferences(true); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + myPatientDao.create(p).getId(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + myPatientDao.create(p).getId(); + + Observation o = new Observation(); + o.getCode().setText("Some Observation"); + o.getSubject().setReference("Patient?identifier=urn%3Asystem%7C" + methodName); + request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST); + + try { + mySystemDao.transaction(myRequestDetails, request); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithNoMatches\" - Multiple resources match this search", e.getMessage()); + } + } + + @Test + public void testTransactionCreateInlineMatchUrlWithTwoMatches() { + String methodName = "testTransactionCreateInlineMatchUrlWithTwoMatches"; + Bundle request = new Bundle(); + + myDaoConfig.setAllowInlineMatchUrlReferences(true); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + myPatientDao.create(p).getId(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + myPatientDao.create(p).getId(); + + Observation o = new Observation(); + o.getCode().setText("Some Observation"); + o.getSubject().setReference("Patient?identifier=urn%3Asystem%7C" + methodName); + request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST); + + try { + mySystemDao.transaction(myRequestDetails, request); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithTwoMatches\" - Multiple resources match this search", e.getMessage()); + } + } + @Test public void testTransactionCreateMatchUrlWithTwoMatch() { String methodName = "testTransactionCreateMatchUrlWithTwoMatch"; @@ -374,7 +468,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1")); assertEquals("1", respEntry.getResponse().getEtag()); - o = (Observation) myObservationDao.read(new IdType(respEntry.getResponse().getLocationElement())); + o = myObservationDao.read(new IdType(respEntry.getResponse().getLocationElement())); assertEquals(new IdType(patientId).toUnqualifiedVersionless().getValue(), o.getSubject().getReference()); } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index c8b718df305..0cdc9fe7462 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -50,6 +50,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { retVal.setSubscriptionPollDelay(5000); retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); + retVal.setAllowInlineMatchUrlReferences(true); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index 42f20d2d5dc..e1b92db9eda 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -46,6 +46,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { retVal.setSubscriptionPollDelay(5000); retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); + retVal.setAllowInlineMatchUrlReferences(true); return retVal; } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index e5b445fe831..0756308baf5 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -59,6 +59,13 @@ JPA server now supports :above and :below qualifiers on URI search params + + Add optional support (disabled by default for now) to JPA server to support + inline references containing search URLs. These URLs will be resolved when + a resource is being created/updated and replaced with the single matching + resource. This is being used as a part of the May 2016 Connectathon for + a testing scenario. +