diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index dd11be7a8d1..a5b61eacef2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -6,6 +6,9 @@ import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + /* * #%L * HAPI FHIR - Core Library @@ -158,5 +161,92 @@ public class UrlUtil { throw new Error("UTF-8 not supported on this platform"); } } + + //@formatter:off + /** + * Parse a URL in one of the following forms: + * + */ + //@formatter:on + public static UrlParts parseUrl(String theUrl) { + UrlParts retVal = new UrlParts(); + + int nextStart = 0; + boolean nextIsHistory = false; + + for (int idx = 0; idx < theUrl.length(); idx++) { + char nextChar = theUrl.charAt(idx); + boolean atEnd = (idx + 1) == theUrl.length(); + if (nextChar == '?' || nextChar == '/' || atEnd) { + int endIdx = atEnd ? idx + 1 : idx; + String nextSubstring = theUrl.substring(nextStart, endIdx); + if (retVal.getResourceType() == null) { + retVal.setResourceType(nextSubstring); + } else if (retVal.getResourceId() == null) { + retVal.setResourceId(nextSubstring); + } else if (nextIsHistory) { + retVal.setVersionId(nextSubstring); + } else { + if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) { + nextIsHistory = true; + } else { + throw new InvalidRequestException("Invalid FHIR resource URL: " + theUrl); + } + } + if (nextChar == '?') { + if (theUrl.length() > idx + 1) { + retVal.setParams(theUrl.substring(idx + 1, theUrl.length())); + } + break; + } + nextStart = idx + 1; + } + } + + return retVal; + } + + public static class UrlParts { + private String myParams; + private String myResourceId; + private String myResourceType; + private String myVersionId; + + public String getParams() { + return myParams; + } + + public String getResourceId() { + return myResourceId; + } + + public String getResourceType() { + return myResourceType; + } + + public String getVersionId() { + return myVersionId; + } + + public void setParams(String theParams) { + myParams = theParams; + } + + public void setResourceId(String theResourceId) { + myResourceId = theResourceId; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public void setVersionId(String theVersionId) { + myVersionId = theVersionId; + } + } } diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 9237ebd9ce4..df590b1415e 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -284,72 +284,6 @@ alphabetical - - de.juplo - hibernate4-maven-plugin - - true - SCRIPT - ${skip-hib4} - - - - - o10g - - export - - test - - org.hibernate.dialect.Oracle10gDialect - ${project.build.directory}/schema_oracle_10g.sql - - - - derby - - export - - test - - org.hibernate.dialect.DerbyTenSevenDialect - ${project.build.directory}/schema_derby.sql - - - - hsql - - export - - test - - org.hibernate.dialect.HSQLDialect - ${project.build.directory}/schema_hsql.sql - - - - mysql5 - - export - - test - - org.hibernate.dialect.MySQL5Dialect - ${project.build.directory}/schema_mysql_5.sql - - - - ca.uhn.hapi.fhir hapi-tinder-plugin @@ -427,6 +361,79 @@ true + + DIST + + + + de.juplo + hibernate4-maven-plugin + + true + SCRIPT + ${skip-hib4} + + + + + o10g + + export + + test + + org.hibernate.dialect.Oracle10gDialect + ${project.build.directory}/schema_oracle_10g.sql + + + + derby + + export + + test + + org.hibernate.dialect.DerbyTenSevenDialect + ${project.build.directory}/schema_derby.sql + + + + hsql + + export + + test + + org.hibernate.dialect.HSQLDialect + ${project.build.directory}/schema_hsql.sql + + + + mysql5 + + export + + test + + org.hibernate.dialect.MySQL5Dialect + ${project.build.directory}/schema_mysql_5.sql + + + + + + + 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 f0c4503a175..21f7c0866ba 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 @@ -87,6 +87,7 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; import ca.uhn.fhir.jpa.entity.ResourceLink; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTag; +import ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource; import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagTypeEnum; import ca.uhn.fhir.jpa.util.StopWatch; @@ -520,7 +521,7 @@ public abstract class BaseHapiFhirDao implements IDao { } throw e; } - IResource resource = (IResource) toResource(type.getImplementingClass(), next); + IResource resource = (IResource) toResource(type.getImplementingClass(), next, true); retVal.add(resource); } return retVal; @@ -558,12 +559,6 @@ public abstract class BaseHapiFhirDao implements IDao { } protected void populateResourceIntoEntity(IResource theResource, ResourceTable theEntity) { - - if (theEntity.getPublished().isEmpty()) { - theEntity.setPublished(new Date()); - } - theEntity.setUpdated(new Date()); - theEntity.setResourceType(toResourceName(theResource)); List refs = myContext.newTerser().getAllPopulatedChildElementsOfType(theResource, BaseResourceReferenceDt.class); @@ -936,13 +931,13 @@ public abstract class BaseHapiFhirDao implements IDao { return retVal; } - protected IBaseResource toResource(BaseHasResource theEntity) { + protected IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) { RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType()); - return toResource(type.getImplementingClass(), theEntity); + return toResource(type.getImplementingClass(), theEntity, theForHistoryOperation); } @SuppressWarnings("unchecked") - protected R toResource(Class theResourceType, BaseHasResource theEntity) { + protected R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { String resourceText = null; switch (theEntity.getEncoding()) { case JSON: @@ -983,9 +978,23 @@ public abstract class BaseHapiFhirDao implements IDao { res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance(); retVal = (R) res; ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); - ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE); + 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); + } } - + res.setId(theEntity.getIdDt()); ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); @@ -1063,25 +1072,28 @@ public abstract class BaseHapiFhirDao implements IDao { } } - protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull) { - return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true); + 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 IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion) { - - if (theEntity.getPublished() == null) { - theEntity.setPublished(new Date()); - } - + protected ResourceTable updateEntity(final IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime) { + + /* + * This should be the very first thing.. + */ if (theResource != null) { - validateResourceForStorage((T) theResource); + validateResourceForStorage((T) theResource, theEntity); String resourceType = myContext.getResourceDefinition(theResource).getName(); if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) { throw new UnprocessableEntityException("Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); } } + if (theEntity.getPublished() == null) { + theEntity.setPublished(theUpdateTime); + } + if (theUpdateHistory) { final ResourceHistoryTable historyEntry = theEntity.toHistory(); myEntityManager.persist(historyEntry); @@ -1159,7 +1171,7 @@ public abstract class BaseHapiFhirDao implements IDao { links = extractResourceLinks(theEntity, theResource); populateResourceIntoEntity(theResource, theEntity); - theEntity.setUpdated(new Date()); + theEntity.setUpdated(theUpdateTime); theEntity.setLanguage(theResource.getLanguage().getValue()); theEntity.setParamsString(stringParams); theEntity.setParamsStringPopulated(stringParams.isEmpty() == false); @@ -1178,11 +1190,11 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setResourceLinks(links); theEntity.setHasLinks(links.isEmpty() == false); theEntity.setIndexStatus(INDEX_STATUS_INDEXED); - + } else { populateResourceIntoEntity(theResource, theEntity); - theEntity.setUpdated(new Date()); + theEntity.setUpdated(theUpdateTime); theEntity.setLanguage(theResource.getLanguage().getValue()); theEntity.setIndexStatus(null); @@ -1197,8 +1209,24 @@ public abstract class BaseHapiFhirDao implements IDao { myEntityManager.persist(theEntity.getForcedId()); } + postPersist(theEntity, (T) theResource); + } else { theEntity = myEntityManager.merge(theEntity); + + postUpdate(theEntity, (T) theResource); + } + + /* + * When subscription is enabled, for each resource we store we also + * store a subscription candidate. These are examined by the subscription + * module and then deleted. + */ + if (myConfig.isSubscriptionEnabled() && thePerformIndexing) { + SubscriptionCandidateResource candidate = new SubscriptionCandidateResource(); + candidate.setResource(theEntity); + candidate.setResourceVersion(theEntity.getVersion()); + myEntityManager.persist(candidate); } if (thePerformIndexing) { @@ -1289,6 +1317,30 @@ public abstract class BaseHapiFhirDao implements IDao { return theEntity; } + /** + * 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 + } + + /** + * 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 + } + /** * 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 @@ -1296,8 +1348,9 @@ public abstract class BaseHapiFhirDao implements IDao { * * @param theResource * The resource that is about to be persisted + * @param theEntityToSave TODO */ - protected void validateResourceForStorage(T theResource) { + protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) { IResource res = (IResource) theResource; TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); if (tagList != null) { 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 bd9977b30e4..f3baeae53c2 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 @@ -142,7 +142,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); @PersistenceContext(type = PersistenceContextType.TRANSACTION) - private EntityManager myEntityManager; + protected EntityManager myEntityManager; @Autowired private PlatformTransactionManager myPlatformTransactionManager; @@ -1004,7 +1004,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH } } - return doCreate(theResource, theIfNoneExist, thePerformIndexing); + return doCreate(theResource, theIfNoneExist, thePerformIndexing, new Date()); } private Predicate createCompositeParamPart(CriteriaBuilder builder, Root from, RuntimeSearchParam left, IQueryParameterType leftValue) { @@ -1268,7 +1268,8 @@ public abstract class BaseHapiFhirResourceDao extends BaseH ActionRequestDetails requestDetails = new ActionRequestDetails(theId, theId.getResourceType()); notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); - ResourceTable savedEntity = updateEntity(null, entity, true, new Date()); + Date updateTime = new Date(); + ResourceTable savedEntity = updateEntity(null, entity, true, updateTime, updateTime); notifyWriteCompleted(); @@ -1298,14 +1299,15 @@ public abstract class BaseHapiFhirResourceDao extends BaseH notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); // Perform delete - ResourceTable savedEntity = updateEntity(null, entity, true, new Date()); + Date updateTime = new Date(); + ResourceTable savedEntity = updateEntity(null, entity, true, updateTime, updateTime); notifyWriteCompleted(); ourLog.info("Processed delete on {} in {}ms", theUrl, w.getMillisAndRestart()); return toMethodOutcome(savedEntity, null); } - private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing) { + private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime) { StopWatch w = new StopWatch(); preProcessResourceForStorage(theResource); @@ -1346,7 +1348,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH ActionRequestDetails requestDetails = new ActionRequestDetails(theResource.getId(), toResourceName(theResource), theResource); notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails); - updateEntity(theResource, entity, false, null, thePerformIndexing, true); + updateEntity(theResource, entity, false, null, thePerformIndexing, true, theUpdateTime); DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); @@ -1419,7 +1421,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH try { BaseHasResource entity = readEntity(theId.toVersionless(), false); validateResourceType(entity); - currentTmp = toResource(myResourceType, entity); + currentTmp = toResource(myResourceType, entity, true); if (ResourceMetadataKeyEnum.UPDATED.get(currentTmp).after(end.getValue())) { currentTmp = null; } @@ -1496,7 +1498,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH if (retVal.size() == maxResults) { break; } - retVal.add(toResource(myResourceType, next)); + retVal.add(toResource(myResourceType, next, true)); } return retVal; @@ -1527,7 +1529,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH return retVal; } - private void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids) { + private void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation) { if (theIncludePids.isEmpty()) { return; } @@ -1546,7 +1548,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH for (ResourceTable next : q.getResultList()) { Class resourceType = getContext().getResourceDefinition(next.getResourceType()).getImplementingClass(); - IResource resource = (IResource) toResource(resourceType, next); + IResource resource = (IResource) toResource(resourceType, next, theForHistoryOperation); Integer index = position.get(next.getId()); if (index == null) { ourLog.warn("Got back unexpected resource PID {}", next.getId()); @@ -1827,7 +1829,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH BaseHasResource entity = readEntity(theId); validateResourceType(entity); - T retVal = toResource(myResourceType, entity); + T retVal = toResource(myResourceType, entity, false); InstantDt deleted = ResourceMetadataKeyEnum.DELETED_AT.get(retVal); if (deleted != null && !deleted.isEmpty()) { @@ -2065,7 +2067,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH // Execute the query and make sure we return distinct results List retVal = new ArrayList(); - loadResourcesByPid(pidsSubList, retVal, revIncludedPids); + loadResourcesByPid(pidsSubList, retVal, revIncludedPids, false); return retVal; } @@ -2381,7 +2383,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH if (resourceId.isIdPartValidLong()) { throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart())); } - return doCreate(theResource, null, thePerformIndexing); + return doCreate(theResource, null, thePerformIndexing, new Date()); } } @@ -2398,7 +2400,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails); // Perform update - ResourceTable savedEntity = updateEntity(theResource, entity, true, null, thePerformIndexing, true); + ResourceTable savedEntity = updateEntity(theResource, entity, true, null, thePerformIndexing, true, new Date()); notifyWriteCompleted(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index dc93764ff3b..02749018d08 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -101,7 +101,7 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao myInterceptors; private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC; + private boolean mySubscriptionEnabled; /** * See {@link #setIncludeLimit(int)} @@ -64,6 +65,13 @@ public class DaoConfig { return myResourceEncoding; } + /** + * See {@link #setSubscriptionEnabled(boolean)} + */ + public boolean isSubscriptionEnabled() { + return mySubscriptionEnabled; + } + public void setHardSearchLimit(int theHardSearchLimit) { myHardSearchLimit = theHardSearchLimit; } @@ -90,12 +98,12 @@ public class DaoConfig { * ID). *

*/ - public void setInterceptors(List theInterceptors) { - myInterceptors = theInterceptors; - } - - public void setResourceEncoding(ResourceEncodingEnum theResourceEncoding) { - myResourceEncoding = theResourceEncoding; + public void setInterceptors(IServerInterceptor... theInterceptor) { + if (theInterceptor == null || theInterceptor.length==0){ + setInterceptors(new ArrayList()); + } else { + setInterceptors(Arrays.asList(theInterceptor)); + } } /** @@ -107,12 +115,23 @@ public class DaoConfig { * ID). *

*/ - public void setInterceptors(IServerInterceptor... theInterceptor) { - if (theInterceptor == null || theInterceptor.length==0){ - setInterceptors(new ArrayList()); - } else { - setInterceptors(Arrays.asList(theInterceptor)); - } + public void setInterceptors(List theInterceptors) { + myInterceptors = theInterceptors; + } + + public void setResourceEncoding(ResourceEncodingEnum theResourceEncoding) { + myResourceEncoding = theResourceEncoding; + } + + /** + * Does this server support subscription? If set to true, the server + * will enable the subscription monitoring mode, which adds a bit of + * overhead. Note that if this is enabled, you must also include + * Spring task scanning to your XML config for the scheduled tasks + * used by the subscription module. + */ + public void setSubscriptionEnabled(boolean theSubscriptionEnabled) { + mySubscriptionEnabled = theSubscriptionEnabled; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java index 0daa1bc776c..2fa92c6f9d6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java @@ -27,6 +27,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; @@ -58,8 +59,8 @@ public class FhirResourceDaoQuestionnaireResponseDstu2 extends FhirResourceDaoDs } @Override - protected void validateResourceForStorage(QuestionnaireResponse theResource) { - super.validateResourceForStorage(theResource); + protected void validateResourceForStorage(QuestionnaireResponse theResource, ResourceTable theEntityToSave) { + super.validateResourceForStorage(theResource, theEntityToSave); if (!myValidateResponses) { return; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java new file mode 100644 index 00000000000..32d72e2196f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java @@ -0,0 +1,195 @@ +package ca.uhn.fhir.jpa.dao; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.persistence.Query; +import javax.persistence.TypedQuery; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.entity.SubscriptionTable; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.UrlUtil.UrlParts; + +public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2implements IFhirResourceDaoSubscription { + + private static final ResourceMetadataKeyEnum ALLOW_STATUS_CHANGE = new ResourceMetadataKeyEnum(FhirResourceDaoSubscriptionDstu2.class.getName() + "_ALLOW_STATUS_CHANGE") { + private static final long serialVersionUID = 1; + + @Override + public Object get(IResource theResource) { + throw new UnsupportedOperationException(); + } + + @Override + public void put(IResource theResource, Object theObject) { + throw new UnsupportedOperationException(); + } + }; + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSubscriptionDstu2.class); + + private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) { + SubscriptionTable subscriptionEntity = new SubscriptionTable(); + subscriptionEntity.setSubscriptionResource(theEntity); + subscriptionEntity.setNextCheck(theEntity.getPublished().getValue()); + subscriptionEntity.setNextCheckSince(theEntity.getPublished().getValue()); + subscriptionEntity.setStatus(theSubscription.getStatusElement().getValueAsEnum()); + myEntityManager.persist(subscriptionEntity); + } + + @Override + public SubscriptionTable getSubscriptionByResourceId(long theSubscriptionResourceId) { + TypedQuery q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_GET_BY_RES", SubscriptionTable.class); + q.setParameter("res_id", theSubscriptionResourceId); + return q.getSingleResult(); + } + + + + @Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) + @Transactional(propagation = Propagation.NOT_SUPPORTED) + @Override + public void pollForNewUndeliveredResources() { + if (getConfig().isSubscriptionEnabled() == false) { + return; + } + ourLog.trace("Beginning pollForNewUndeliveredResources()"); + + TypedQuery q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_NEXT_CHECK", SubscriptionTable.class); + q.setParameter("next_check", new Date()); + q.setParameter("status", SubscriptionStatusEnum.ACTIVE); + List subscriptions = q.getResultList(); + + + } + + @Override + protected void postPersist(ResourceTable theEntity, Subscription theSubscription) { + super.postPersist(theEntity, theSubscription); + + createSubscriptionTable(theEntity, theSubscription); + } + + @Override + public void setSubscriptionStatus(Long theResourceId, SubscriptionStatusEnum theStatus) { + Validate.notNull(theResourceId); + Validate.notNull(theStatus); + + ResourceTable existing = readEntityLatestVersion(new IdDt("Subscription", theResourceId)); + Subscription existingRes = toResource(Subscription.class, existing, false); + + existingRes.getResourceMetadata().put(ALLOW_STATUS_CHANGE, new Object()); + existingRes.setStatus(theStatus); + + update(existingRes); + } + + @Override + protected ResourceTable updateEntity(IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime) { + ResourceTable retVal = super.updateEntity(theResource, theEntity, theUpdateHistory, theDeletedTimestampOrNull, thePerformIndexing, theUpdateVersion, theUpdateTime); + + Subscription resource = (Subscription) theResource; + Long resourceId = theEntity.getId(); + if (theDeletedTimestampOrNull != null) { + Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_DELETE"); + q.setParameter("res_id", resourceId); + q.executeUpdate(); + } else { + Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_SET_STATUS"); + q.setParameter("res_id", resourceId); + q.setParameter("status", resource.getStatusElement().getValueAsEnum()); + if (q.executeUpdate() > 0) { + ourLog.info("Updated subscription status for subscription {} to {}", resourceId, resource.getStatusElement().getValueAsEnum()); + } else { + createSubscriptionTable(retVal, resource); + } + } + return retVal; + } + + @Override + protected void validateResourceForStorage(Subscription theResource, ResourceTable theEntityToSave) { + super.validateResourceForStorage(theResource, theEntityToSave); + + String query = theResource.getCriteria(); + if (isBlank(query)) { + throw new UnprocessableEntityException("Subscription.criteria must be populated"); + } + + int sep = query.indexOf('?'); + if (sep <= 1) { + throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\""); + } + + String resType = query.substring(0, sep); + if (resType.contains("/")) { + throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\""); + } + + RuntimeResourceDefinition resDef; + try { + resDef = getContext().getResourceDefinition(resType); + } catch (DataFormatException e) { + throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resType); + } + + IFhirResourceDao dao = getDao(resDef.getImplementingClass()); + if (dao == null) { + throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resDef); + } + +// SearchParameterMap parsedUrl = translateMatchUrl(query, resDef); + + if (theResource.getChannel().getType() == null) { + throw new UnprocessableEntityException("Subscription.channel.type must be populated on this server"); + } + + SubscriptionStatusEnum status = theResource.getStatusElement().getValueAsEnum(); + Subscription existing = theEntityToSave.getEncoding() != null ? toResource(Subscription.class, theEntityToSave, false) : null; + if (status == null) { + // if (existing != null) { + // status = existing.getStatusElement().getValueAsEnum(); + // theResource.setStatus(status); + // } else { + status = SubscriptionStatusEnum.REQUESTED; + theResource.setStatus(status); + // } + } else { + SubscriptionStatusEnum existingStatus = existing.getStatusElement().getValueAsEnum(); + if (existingStatus != status) { + if (!theResource.getResourceMetadata().containsKey(ALLOW_STATUS_CHANGE)) { + throw new UnprocessableEntityException("Subscription.status can not be changed from " + existingStatus + " to " + status); + } + } + } + + if (theEntityToSave.getId() == null) { + if (status != SubscriptionStatusEnum.REQUESTED) { + throw new UnprocessableEntityException("Subscription.status must be " + SubscriptionStatusEnum.REQUESTED.getCode() + " on a newly created subscription"); + } + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java index aa5e3361a1a..48a5374251a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -60,7 +60,7 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 if (sourceEntity == null) { throw new ResourceNotFoundException(theId); } - ValueSet source = (ValueSet) toResource(sourceEntity); + ValueSet source = (ValueSet) toResource(sourceEntity, false); /* * Add composed concepts diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1.java index 94be640520c..16f79a65a38 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu1.java @@ -104,6 +104,7 @@ public class FhirSystemDaoDstu1 extends BaseHapiFhirSystemDao> { OperationOutcome oo = new OperationOutcome(); retVal.add(oo); + Date updateTime = new Date(); for (int resourceIdx = 0; resourceIdx < theResources.size(); resourceIdx++) { IResource nextResource = theResources.get(resourceIdx); @@ -160,6 +161,8 @@ public class FhirSystemDaoDstu1 extends BaseHapiFhirSystemDao> { if (entity == null) { nextResouceOperationOut = BundleEntryTransactionMethodEnum.POST; entity = toEntity(nextResource); + entity.setUpdated(updateTime); + entity.setPublished(updateTime); if (nextId.isEmpty() == false && "cid:".equals(nextId.getBaseUrl())) { ourLog.debug("Resource in transaction has ID[{}], will replace with server assigned ID", nextId.getIdPart()); } else if (nextResouceOperationIn == BundleEntryTransactionMethodEnum.POST) { @@ -170,7 +173,7 @@ public class FhirSystemDaoDstu1 extends BaseHapiFhirSystemDao> { if (candidateMatches.size() == 1) { ourLog.debug("Resource with match URL [{}] already exists, will be NOOP", matchUrl); BaseHasResource existingEntity = loadFirstEntityFromCandidateMatches(candidateMatches); - IResource existing = (IResource) toResource(existingEntity); + IResource existing = (IResource) toResource(existingEntity, false); persistedResources.add(null); retVal.add(existing); continue; @@ -262,11 +265,11 @@ public class FhirSystemDaoDstu1 extends BaseHapiFhirSystemDao> { InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(resource); Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; if (deletedInstantOrNull == null && ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(resource) == BundleEntryTransactionMethodEnum.DELETE) { - deletedTimestampOrNull = new Date(); + deletedTimestampOrNull = updateTime; ResourceMetadataKeyEnum.DELETED_AT.put(resource, new InstantDt(deletedTimestampOrNull)); } - updateEntity(resource, table, table.getId() != null, deletedTimestampOrNull); + updateEntity(resource, table, table.getId() != null, deletedTimestampOrNull, updateTime); } long delay = System.currentTimeMillis() - start; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 126f175d6e3..27a72c16d14 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -19,7 +19,9 @@ package ca.uhn.fhir.jpa.dao; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.Date; import java.util.HashMap; @@ -64,6 +66,8 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.UrlUtil.UrlParts; public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu2.class); @@ -92,10 +96,10 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { resp.addEntry().setResource(ooResp); /* - * For batch, we handle each entry as a mini-transaction in its own - * database transaction so that if one fails, it doesn't prevent others + * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it + * doesn't prevent others */ - + for (final Entry nextRequestEntry : theRequest.getEntry()) { TransactionCallback callback = new TransactionCallback() { @@ -118,13 +122,13 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { Entry subResponseEntry = nextResponseBundle.getEntry().get(0); resp.addEntry(subResponseEntry); /* - * If the individual entry didn't have a resource in its response, bring the - * sub-transaction's OperationOutcome across so the client can see it + * If the individual entry didn't have a resource in its response, bring the sub-transaction's + * OperationOutcome across so the client can see it */ if (subResponseEntry.getResource() == null) { subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource()); } - + } catch (BaseServerResponseException e) { caughtEx = e; } catch (Throwable t) { @@ -167,75 +171,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { return retVal; } - private UrlParts parseUrl(String theAction, String theUrl) { - UrlParts retVal = new UrlParts(); - - //@formatter:off - /* - * We assume that the URL passed in is in one of the following forms: - * [Resource Type]?[Search Params] - * [Resource Type]/[Resource ID] - * [Resource Type]/[Resource ID]/_history/[Version ID] - */ - //@formatter:on - int nextStart = 0; - boolean nextIsHistory = false; - - for (int idx = 0; idx < theUrl.length(); idx++) { - char nextChar = theUrl.charAt(idx); - boolean atEnd = (idx + 1) == theUrl.length(); - if (nextChar == '?' || nextChar == '/' || atEnd) { - int endIdx = atEnd ? idx + 1 : idx; - String nextSubstring = theUrl.substring(nextStart, endIdx); - if (retVal.getResourceType() == null) { - retVal.setResourceType(nextSubstring); - } else if (retVal.getResourceId() == null) { - retVal.setResourceId(nextSubstring); - } else if (nextIsHistory) { - retVal.setVersionId(nextSubstring); - } else { - if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) { - nextIsHistory = true; - } else { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theAction, theUrl); - throw new InvalidRequestException(msg); - } - } - if (nextChar == '?') { - if (theUrl.length() > idx + 1) { - retVal.setParams(theUrl.substring(idx + 1, theUrl.length())); - } - break; - } - nextStart = idx + 1; - } - } - - RuntimeResourceDefinition resType; - try { - resType = getContext().getResourceDefinition(retVal.getResourceType()); - } catch (DataFormatException e) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theAction, theUrl); - throw new InvalidRequestException(msg); - } - IFhirResourceDao dao = null; - if (resType != null) { - dao = getDao(resType.getImplementingClass()); - } - if (dao == null) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theAction, theUrl); - throw new InvalidRequestException(msg); - } - retVal.setDao(dao); - - if (retVal.getResourceId() == null && retVal.getParams() == null) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theAction, theUrl); - throw new InvalidRequestException(msg); - } - - return retVal; - } - @Transactional(propagation = Propagation.REQUIRED) @Override public Bundle transaction(Bundle theRequest) { @@ -265,6 +200,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); long start = System.currentTimeMillis(); + Date updateTime = new Date(); Set allIds = new LinkedHashSet(); Map idSubstitutions = new HashMap(); @@ -275,11 +211,11 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { // TODO: process verbs in the correct order for (int i = 0; i < theRequest.getEntry().size(); i++) { - + if (i % 100 == 0) { ourLog.info("Processed {} entries out of {}", i, theRequest.getEntry().size()); } - + Entry nextEntry = theRequest.getEntry().get(i); IResource res = nextEntry.getResource(); IdDt nextResourceId = null; @@ -330,11 +266,12 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { // DELETE Entry newEntry = response.addEntry(); String url = extractTransactionUrlOrThrowException(nextEntry, verb); - UrlParts parts = parseUrl(verb.getCode(), url); + UrlParts parts = UrlUtil.parseUrl(url); + ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb.getCode(), url); if (parts.getResourceId() != null) { - parts.getDao().delete(new IdDt(parts.getResourceType(), parts.getResourceId())); + dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId())); } else { - parts.getDao().deleteByUrl(parts.getResourceType() + '?' + parts.getParams()); + dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams()); } newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_204_NO_CONTENT)); @@ -350,7 +287,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { String url = extractTransactionUrlOrThrowException(nextEntry, verb); - UrlParts parts = parseUrl(verb.getCode(), url); + UrlParts parts = UrlUtil.parseUrl(url); if (isNotBlank(parts.getResourceId())) { res.setId(new IdDt(parts.getResourceType(), parts.getResourceId())); outcome = resourceDao.update(res, null, false); @@ -365,10 +302,10 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { case GET: { // SEARCH/READ/VREAD String url = extractTransactionUrlOrThrowException(nextEntry, verb); - UrlParts parts = parseUrl(verb.getCode(), url); + UrlParts parts = UrlUtil.parseUrl(url); @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = parts.getDao(); + IFhirResourceDao dao = toDao(parts, verb.getCode(), url); String ifNoneMatch = nextEntry.getRequest().getIfNoneMatch(); if (isNotBlank(ifNoneMatch)) { @@ -382,9 +319,9 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { if (isNotBlank(ifNoneMatch)) { throw new InvalidRequestException("Unable to perform vread on '" + url + "' with ifNoneMatch also set. Do not include a version in the URL to perform a conditional read."); } - found = (IResource) resourceDao.read(new IdDt(parts.getResourceType(), parts.getResourceId(), parts.getVersionId())); + found = (IResource) dao.read(new IdDt(parts.getResourceType(), parts.getResourceId(), parts.getVersionId())); } else { - found = (IResource) resourceDao.read(new IdDt(parts.getResourceType(), parts.getResourceId())); + found = (IResource) dao.read(new IdDt(parts.getResourceType(), parts.getResourceId())); if (isNotBlank(ifNoneMatch) && ifNoneMatch.equals(found.getId().getVersionIdPart())) { notChanged = true; } @@ -402,9 +339,9 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { resp.setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); } } else if (parts.getParams() != null) { - RuntimeResourceDefinition def = getContext().getResourceDefinition(parts.getDao().getResourceType()); + RuntimeResourceDefinition def = getContext().getResourceDefinition(dao.getResourceType()); SearchParameterMap params = translateMatchUrl(url, def); - IBundleProvider bundle = parts.getDao().search(params); + IBundleProvider bundle = dao.search(params); Bundle searchBundle = new Bundle(); searchBundle.setTotal(bundle.size()); @@ -453,7 +390,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource); Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; - updateEntity(nextResource, nextOutcome.getEntity(), false, deletedTimestampOrNull, true, false); + updateEntity(nextResource, nextOutcome.getEntity(), false, deletedTimestampOrNull, true, false, updateTime); } myEntityManager.flush(); @@ -468,8 +405,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { IFhirResourceDao resourceDao = getDao(nextEntry.getResource().getClass()); Set val = resourceDao.processMatchUrl(matchUrl); if (val.size() > 1) { - throw new InvalidRequestException( - "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); + throw new InvalidRequestException("Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); } } } @@ -488,13 +424,38 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { long delay = System.currentTimeMillis() - start; ourLog.info(theActionName + " completed in {}ms", new Object[] { delay }); - + notifyWriteCompleted(); response.setType(BundleTypeEnum.TRANSACTION_RESPONSE); return response; } + private ca.uhn.fhir.jpa.dao.IFhirResourceDao toDao(UrlParts theParts, String theVerb, String theUrl) { + RuntimeResourceDefinition resType; + try { + resType = getContext().getResourceDefinition(theParts.getResourceType()); + } catch (DataFormatException e) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + throw new InvalidRequestException(msg); + } + IFhirResourceDao dao = null; + if (resType != null) { + dao = getDao(resType.getImplementingClass()); + } + if (dao == null) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + throw new InvalidRequestException(msg); + } + + if (theParts.getResourceId() == null && theParts.getParams() == null) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + throw new InvalidRequestException(msg); + } + + return dao; + } + private IFhirResourceDao getDaoOrThrowException(Class theClass) { IFhirResourceDao retVal = getDao(theClass); if (retVal == null) { @@ -503,15 +464,15 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { return retVal; } - private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome, - Entry newEntry, String theResourceType, IResource theRes) { + private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome, Entry newEntry, String theResourceType, IResource theRes) { IdDt newId = (IdDt) outcome.getId().toUnqualifiedVersionless(); IdDt resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); if (newId.equals(resourceId) == false) { idSubstitutions.put(resourceId, newId); if (isPlaceholder(resourceId)) { /* - * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. + * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified + * kind too just to be lenient. */ idSubstitutions.put(new IdDt(theResourceType + '/' + resourceId.getValue()), newId); } @@ -538,52 +499,4 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); } - private static class UrlParts { - private IFhirResourceDao myDao; - private String myParams; - private String myResourceId; - private String myResourceType; - private String myVersionId; - - public IFhirResourceDao getDao() { - return myDao; - } - - public String getParams() { - return myParams; - } - - public String getResourceId() { - return myResourceId; - } - - public String getResourceType() { - return myResourceType; - } - - public String getVersionId() { - return myVersionId; - } - - public void setDao(IFhirResourceDao theDao) { - myDao = theDao; - } - - public void setParams(String theParams) { - myParams = theParams; - } - - public void setResourceId(String theResourceId) { - myResourceId = theResourceId; - } - - public void setResourceType(String theResourceType) { - myResourceType = theResourceType; - } - - public void setVersionId(String theVersionId) { - myVersionId = theVersionId; - } - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java new file mode 100644 index 00000000000..4700011b9b8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jpa.dao; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2015 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.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.jpa.entity.SubscriptionTable; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; + +public interface IFhirResourceDaoSubscription extends IFhirResourceDao { + + void pollForNewUndeliveredResources(); + + void setSubscriptionStatus(Long theResourceId, SubscriptionStatusEnum theStatus); + + SubscriptionTable getSubscriptionByResourceId(long theSubscriptionResourceId); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java index c161109b2ec..62efd040b37 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java @@ -98,7 +98,11 @@ public abstract class BaseHasResource { public abstract IdDt getIdDt(); public InstantDt getPublished() { - return new InstantDt(myPublished); + if (myPublished != null) { + return new InstantDt(myPublished); + } else { + return null; + } } public byte[] getResource() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionCandidateResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionCandidateResource.java new file mode 100644 index 00000000000..02616288289 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionCandidateResource.java @@ -0,0 +1,47 @@ +package ca.uhn.fhir.jpa.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +@Entity +@Table(name = "HFJ_SUBSCRIPTION_CAND_RES") +public class SubscriptionCandidateResource { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_SUBSCRIPTION_CAND_ID", sequenceName = "SEQ_SUBSCRIPTION_CAND_ID") + @Column(name = "PID", insertable = false, updatable = false) + private Long myId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID") + private ResourceTable myResource; + + @Column(name = "RES_VERSION", nullable = false) + private long myResourceVersion; + + public ResourceTable getResource() { + return myResource; + } + + public long getResourceVersion() { + return myResourceVersion; + } + + public void setResource(ResourceTable theResource) { + myResource = theResource; + } + + public void setResourceVersion(long theResourceVersion) { + myResourceVersion = theResourceVersion; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionFlaggedResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionFlaggedResource.java new file mode 100644 index 00000000000..73a7ae2f679 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionFlaggedResource.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.jpa.entity; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "HFJ_SUBSCRIPTION_FLAG_RES") +public class SubscriptionFlaggedResource { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_SUBSCRIPTION_FLAG_ID", sequenceName = "SEQ_SUBSCRIPTION_FLAG_ID") + @Column(name = "PID", insertable = false, updatable = false) + private Long myId; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "CREATED", nullable = false) + private Date myCreated; + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java new file mode 100644 index 00000000000..e602038f1c1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java @@ -0,0 +1,112 @@ +package ca.uhn.fhir.jpa.entity; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; + +//@formatter:off +@Entity +@Table(name = "HFJ_SUBSCRIPTION", uniqueConstraints= { + @UniqueConstraint(name="IDX_SUBS_RESID", columnNames= { "RES_ID" }), + @UniqueConstraint(name="IDX_SUBS_NEXTCHECK", columnNames= { "SUBSCRIPTION_STATUS", "NEXT_CHECK" }) +}) +@NamedQueries({ + @NamedQuery(name="Q_HFJ_SUBSCRIPTION_SET_STATUS", query="UPDATE SubscriptionTable t SET t.myStatus = :status WHERE t.myResId = :res_id"), + @NamedQuery(name="Q_HFJ_SUBSCRIPTION_NEXT_CHECK", query="SELECT t FROM SubscriptionTable t WHERE t.myStatus = :status AND t.myNextCheck <= :next_check"), + @NamedQuery(name="Q_HFJ_SUBSCRIPTION_GET_BY_RES", query="SELECT t FROM SubscriptionTable t WHERE t.myResId = :res_id"), + @NamedQuery(name="Q_HFJ_SUBSCRIPTION_DELETE", query="DELETE FROM SubscriptionTable t WHERE t.myResId = :res_id"), +}) +//@formatter:on +public class SubscriptionTable { + + @Column(name = "CHECK_INTERVAL", nullable = false) + private long myCheckInterval; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "SEQ_SUBSCRIPTION_ID", sequenceName = "SEQ_SUBSCRIPTION_ID") + @Column(name = "PID", insertable = false, updatable = false) + private Long myId; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "NEXT_CHECK", nullable = false) + private Date myNextCheck; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "NEXT_CHECK_SINCE", nullable = false) + private Date myNextCheckSince; + + @Column(name = "RES_ID", insertable = false, updatable = false) + private Long myResId; + + @Column(name = "SUBSCRIPTION_STATUS", nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private SubscriptionStatusEnum myStatus; + + @OneToOne() + @JoinColumn(name = "RES_ID", insertable = true, updatable = false, referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_SUBSCRIPTION_RESOURCE_ID") ) + private ResourceTable mySubscriptionResource; + + public long getCheckInterval() { + return myCheckInterval; + } + + public Long getId() { + return myId; + } + + public Date getNextCheck() { + return myNextCheck; + } + + public Date getNextCheckSince() { + return myNextCheckSince; + } + + public SubscriptionStatusEnum getStatus() { + return myStatus; + } + + public ResourceTable getSubscriptionResource() { + return mySubscriptionResource; + } + + public void setCheckInterval(long theCheckInterval) { + myCheckInterval = theCheckInterval; + } + + public void setNextCheck(Date theNextCheck) { + myNextCheck = theNextCheck; + } + + public void setNextCheckSince(Date theNextCheckSince) { + myNextCheckSince = theNextCheckSince; + } + + public void setStatus(SubscriptionStatusEnum theStatus) { + myStatus = theStatus; + } + + public void setSubscriptionResource(ResourceTable theSubscriptionResource) { + mySubscriptionResource = theSubscriptionResource; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java index e72f4ce64ea..35e88788e3e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java @@ -17,7 +17,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.transaction.TransactionConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +37,9 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; import ca.uhn.fhir.jpa.entity.ResourceLink; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTag; +import ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource; +import ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource; +import ca.uhn.fhir.jpa.entity.SubscriptionTable; import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.model.dstu2.resource.Bundle; @@ -55,6 +57,7 @@ import ca.uhn.fhir.model.dstu2.resource.Practitioner; import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; import ca.uhn.fhir.model.dstu2.resource.StructureDefinition; +import ca.uhn.fhir.model.dstu2.resource.Subscription; import ca.uhn.fhir.model.dstu2.resource.Substance; import ca.uhn.fhir.model.dstu2.resource.ValueSet; import ca.uhn.fhir.parser.IParser; @@ -66,7 +69,6 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; @ContextConfiguration(locations={ "classpath:hapi-fhir-server-resourceproviders-dstu2.xml", "classpath:fhir-jpabase-spring-test-config.xml"}) -@TransactionConfiguration(defaultRollback=false) //@formatter:on public abstract class BaseJpaDstu2Test extends BaseJpaTest { @@ -74,9 +76,6 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myConceptMapDaoDstu2") protected IFhirResourceDao myConceptMapDao; @Autowired - @Qualifier("mySubstanceDaoDstu2") - protected IFhirResourceDao mySubstanceDao; - @Autowired protected DaoConfig myDaoConfig; @Autowired @Qualifier("myDeviceDaoDstu2") @@ -126,6 +125,12 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myStructureDefinitionDaoDstu2") protected IFhirResourceDao myStructureDefinitionDao; @Autowired + @Qualifier("mySubscriptionDaoDstu2") + protected IFhirResourceDaoSubscription mySubscriptionDao; + @Autowired + @Qualifier("mySubstanceDaoDstu2") + protected IFhirResourceDao mySubstanceDao; + @Autowired @Qualifier("mySystemDaoDstu2") protected IFhirSystemDao mySystemDao; @Autowired @@ -160,6 +165,13 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { return newJsonParser.parseResource(type, string); } + public TransactionTemplate newTxTemplate() { + TransactionTemplate retVal = new TransactionTemplate(myTxManager); + retVal.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + retVal.afterPropertiesSet(); + return retVal; + } + public static void purgeDatabase(final EntityManager entityManager, PlatformTransactionManager theTxManager) { TransactionTemplate txTemplate = new TransactionTemplate(theTxManager); txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); @@ -174,6 +186,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { txTemplate.execute(new TransactionCallback() { @Override public Void doInTransaction(TransactionStatus theStatus) { + entityManager.createQuery("DELETE from " + SubscriptionCandidateResource.class.getSimpleName() + " d").executeUpdate(); + entityManager.createQuery("DELETE from " + SubscriptionFlaggedResource.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ForcedId.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d").executeUpdate(); @@ -189,6 +203,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { txTemplate.execute(new TransactionCallback() { @Override public Void doInTransaction(TransactionStatus theStatus) { + entityManager.createQuery("DELETE from " + SubscriptionTable.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceHistoryTag.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceTag.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + TagDefinition.class.getSimpleName() + " d").executeUpdate(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SubscriptionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SubscriptionTest.java new file mode 100644 index 00000000000..830ef005170 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SubscriptionTest.java @@ -0,0 +1,156 @@ +package ca.uhn.fhir.jpa.dao; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Set; + +import javax.persistence.TypedQuery; + +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.Before; +import org.junit.Test; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; + +import ca.uhn.fhir.jpa.entity.SubscriptionTable; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + +public class FhirResourceDaoDstu2SubscriptionTest extends BaseJpaDstu2Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SubscriptionTest.class); + + @Test + public void testCreateSubscriptionInvalidCriteria() { + Subscription subs = new Subscription(); + subs.setCriteria("Observation"); + try { + mySubscriptionDao.create(subs); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Subscription.criteria must be in the form \"{Resource Type}?[params]\"")); + } + + subs = new Subscription(); + subs.setCriteria("http://foo.com/Observation?AAA=BBB"); + try { + mySubscriptionDao.create(subs); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Subscription.criteria must be in the form \"{Resource Type}?[params]\"")); + } + + subs = new Subscription(); + subs.setCriteria("ObservationZZZZ?a=b"); + try { + mySubscriptionDao.create(subs); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Subscription.criteria contains invalid/unsupported resource type: ObservationZZZZ")); + } + + subs = new Subscription(); + subs.setCriteria("Observation?identifier=123"); + try { + mySubscriptionDao.create(subs); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Subscription.channel.type must be populated on this server")); + } + + subs = new Subscription(); + subs.setCriteria("Observation?identifier=123"); + subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET); + assertTrue(mySubscriptionDao.create(subs).getId().hasIdPart()); + + } + + @Before + public void beforeEnableSubscription() { + myDaoConfig.setSubscriptionEnabled(true); + } + + @Test + public void testSubscriptionResourcesAppear() { + String methodName = "testSubscriptionResourcesAppear"; + Patient p = new Patient(); + p.addName().addFamily(methodName); + IIdType pId = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + Observation obs = new Observation(); + obs.getSubject().setReference(pId); + obs.setStatus(ObservationStatusEnum.FINAL); + IIdType beforeId = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); + + Subscription subs = new Subscription(); + subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET); + subs.setCriteria("Observation?subject=Patient/123"); + IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless(); + + obs = new Observation(); + obs.getSubject().setReference(pId); + obs.setStatus(ObservationStatusEnum.FINAL); + IIdType afterId1 = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); + + obs = new Observation(); + obs.getSubject().setReference(pId); + obs.setStatus(ObservationStatusEnum.FINAL); + IIdType afterId2 = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); + + mySubscriptionDao.pollForNewUndeliveredResources(); + } + + @Test + public void testCreateSubscription() { + Subscription subs = new Subscription(); + subs.setCriteria("Observation?subject=Patient/123"); + subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET); + + IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless(); + + TypedQuery q = myEntityManager.createQuery("SELECT t from SubscriptionTable t WHERE t.mySubscriptionResource.myId = :id", SubscriptionTable.class); + q.setParameter("id", id.getIdPartAsLong()); + final SubscriptionTable table = q.getSingleResult(); + + assertNotNull(table); + assertNotNull(table.getNextCheck()); + assertEquals(table.getNextCheck(), table.getSubscriptionResource().getPublished().getValue()); + assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus()); + assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum()); + + mySubscriptionDao.setSubscriptionStatus(id.getIdPartAsLong(), SubscriptionStatusEnum.ACTIVE); + + assertEquals(SubscriptionStatusEnum.ACTIVE, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus()); + assertEquals(SubscriptionStatusEnum.ACTIVE, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum()); + + mySubscriptionDao.delete(id); + + assertNull(myEntityManager.find(SubscriptionTable.class, table.getId())); + + /* + * Re-create again + */ + + subs = new Subscription(); + subs.setCriteria("Observation?subject=Patient/123"); + subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET); + subs.setId(id); + mySubscriptionDao.update(subs); + + assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.createQuery("SELECT t FROM SubscriptionTable t WHERE t.myResId = " + id.getIdPart(), SubscriptionTable.class).getSingleResult().getStatus()); + assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum()); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java index 45b11e2f92a..8051448f839 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java @@ -77,6 +77,7 @@ import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; @@ -966,8 +967,13 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertEquals(id.withVersion("1"), entries.get(2).getIdElement()); assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) entries.get(0))); + assertEquals(BundleEntryTransactionMethodEnum.PUT, ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get((IResource) entries.get(0))); + assertNotNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) entries.get(1))); + assertEquals(BundleEntryTransactionMethodEnum.DELETE, ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get((IResource) entries.get(1))); + assertNull(ResourceMetadataKeyEnum.DELETED_AT.get((IResource) entries.get(2))); + assertEquals(BundleEntryTransactionMethodEnum.POST, ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get((IResource) entries.get(2))); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/resources/META-INF/persistence.xml b/hapi-fhir-jpaserver-base/src/test/resources/META-INF/persistence.xml index f990e6c30cf..bad2178e79b 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/META-INF/persistence.xml +++ b/hapi-fhir-jpaserver-base/src/test/resources/META-INF/persistence.xml @@ -21,6 +21,9 @@ ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords ca.uhn.fhir.jpa.entity.ResourceLink ca.uhn.fhir.jpa.entity.ResourceTag + ca.uhn.fhir.jpa.entity.SubscriptionTable + ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource + ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource ca.uhn.fhir.jpa.entity.TagDefinition false diff --git a/hapi-fhir-jpaserver-example/src/main/resources/META-INF/fhirtest_persistence.xml b/hapi-fhir-jpaserver-example/src/main/resources/META-INF/fhirtest_persistence.xml index 6ec380207c0..3b0edd509bd 100644 --- a/hapi-fhir-jpaserver-example/src/main/resources/META-INF/fhirtest_persistence.xml +++ b/hapi-fhir-jpaserver-example/src/main/resources/META-INF/fhirtest_persistence.xml @@ -20,6 +20,9 @@ ca.uhn.fhir.jpa.entity.ResourceLink ca.uhn.fhir.jpa.entity.ResourceTable ca.uhn.fhir.jpa.entity.ResourceTag + ca.uhn.fhir.jpa.entity.SubscriptionTable + ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource + ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource ca.uhn.fhir.jpa.entity.TagDefinition true diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml index c2dafc4915c..6e9f1f4a0d0 100644 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml @@ -21,7 +21,7 @@ and other properties supported by BasicDataSource. --> - + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/resources/META-INF/fhirtest_persistence.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/resources/META-INF/fhirtest_persistence.xml index a1d85bfa9c8..8fd4f7e4de4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/resources/META-INF/fhirtest_persistence.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/resources/META-INF/fhirtest_persistence.xml @@ -20,6 +20,9 @@ ca.uhn.fhir.jpa.entity.ResourceLink ca.uhn.fhir.jpa.entity.ResourceTable ca.uhn.fhir.jpa.entity.ResourceTag + ca.uhn.fhir.jpa.entity.SubscriptionTable + ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource + ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource ca.uhn.fhir.jpa.entity.TagDefinition true diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/test/resources/fhir_jpatest_persistence.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/test/resources/fhir_jpatest_persistence.xml index 56306183fa6..e655590260a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/test/resources/fhir_jpatest_persistence.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/test/resources/fhir_jpatest_persistence.xml @@ -16,7 +16,10 @@ ca.uhn.fhir.jpa.entity.ResourceLink ca.uhn.fhir.jpa.entity.ResourceTable ca.uhn.fhir.jpa.entity.ResourceTag - + ca.uhn.fhir.jpa.entity.SubscriptionTable + ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource + ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource + true diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 78039287a3c..595ac70b4ed 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -104,12 +104,10 @@ org.springframework spring-webmvc - ${spring_version} org.springframework spring-context - ${spring_version} xml-apis @@ -120,53 +118,44 @@ org.springframework spring-beans - ${spring_version} org.springframework spring-tx - ${spring_version} org.springframework spring-context-support - ${spring_version} org.springframework spring-web - ${spring_version} org.eclipse.jetty jetty-servlets - ${jetty_version} test org.eclipse.jetty jetty-webapp - ${jetty_version} test org.eclipse.jetty jetty-server - ${jetty_version} test org.eclipse.jetty jetty-servlet - ${jetty_version} test org.eclipse.jetty jetty-util - ${jetty_version} test diff --git a/hapi-fhir-testpage-overlay/src/test/resources/META-INF/persistence.xml b/hapi-fhir-testpage-overlay/src/test/resources/META-INF/persistence.xml index 3dcefdcd452..c91d1098ab5 100644 --- a/hapi-fhir-testpage-overlay/src/test/resources/META-INF/persistence.xml +++ b/hapi-fhir-testpage-overlay/src/test/resources/META-INF/persistence.xml @@ -21,6 +21,9 @@ ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords ca.uhn.fhir.jpa.entity.ResourceLink ca.uhn.fhir.jpa.entity.ResourceTag + ca.uhn.fhir.jpa.entity.SubscriptionTable + ca.uhn.fhir.jpa.entity.SubscriptionCandidateResource + ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource ca.uhn.fhir.jpa.entity.TagDefinition false diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm index af2b55a94a1..bb0fa08afcd 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm @@ -33,7 +33,7 @@ #foreach ( $res in $resources ) #else class="ca.uhn.fhir.jpa.dao.FhirResourceDao${versionCapitalized}"> diff --git a/pom.xml b/pom.xml index 574f0aa7cba..743cd97ebba 100644 --- a/pom.xml +++ b/pom.xml @@ -523,6 +523,11 @@ spring-web ${spring_version} + + org.springframework + spring-webmvc + ${spring_version} + org.thymeleaf thymeleaf diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 500eda8e512..55d25a4d84d 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -26,6 +26,10 @@ JPA server did not correctly index search parameters of type "reference" where the path had multiple entries (i.e. "Resource.path1 | Resource.path2") + + JPA server _history operations (server, type, instance) not correctly set the + Bundle.entry.request.method to POST or PUT for create and updates of the resource. +