package ca.uhn.fhir.jpa.dao;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2017 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 static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.*;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.*;
import ca.uhn.fhir.rest.method.SearchMethodBinding.QualifierDetails;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.ResourceReferenceInfo;
@Transactional(propagation = Propagation.REQUIRED)
public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
@Autowired
private DaoConfig myDaoConfig;
@Autowired
protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired
private IResourceHistoryTableDao myResourceHistoryTableDao;
private String myResourceName;
@Autowired
protected IResourceTableDao myResourceTableDao;
private Class<T> myResourceType;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
@Autowired()
protected ISearchResultDao mySearchResultDao;
private String mySecondaryPrimaryKeyParamName;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theId);
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
//@formatter:off
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) &&
ObjectUtil.equals(next.getTag().getSystem(), theScheme) &&
ObjectUtil.equals(next.getTag().getCode(), theTerm)) {
return;
}
}
//@formatter:on
entity.setHasTags(true);
TagDefinition def = getTag(TagTypeEnum.TAG, theScheme, theTerm, theLabel);
BaseTag newEntity = entity.addTag(def);
myEntityManager.persist(newEntity);
myEntityManager.merge(entity);
ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() });
}
@Override
public DaoMethodOutcome create(final T theResource) {
return create(theResource, null, true, null);
}
@Override
public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) {
return create(theResource, null, true, theRequestDetails);
}
@Override
public DaoMethodOutcome create(final T theResource, String theIfNoneExist) {
return create(theResource, theIfNoneExist, null);
}
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, RequestDetails theRequestDetails) {
if (isNotBlank(theResource.getIdElement().getIdPart())) {
if (getContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) {
if (theResource.getIdElement().isIdPartValidLong()) {
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart());
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing"));
}
} else if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing"));
} else {
// As of DSTU3, ID and version in the body should be ignored for a create/update
theResource.setId("");
}
}
return doCreate(theResource, theIfNoneExist, thePerformIndexing, new Date(), theRequestDetails);
}
@Override
public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) {
return create(theResource, theIfNoneExist, true, theRequestDetails);
}
public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) {
return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode);
}
public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) {
return createOperationOutcome(OO_SEVERITY_INFO, theMessage, "informational");
}
protected abstract IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode);
@Override
public DaoMethodOutcome delete(IIdType theId) {
return delete(theId, null);
}
@Override
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided");
}
final ResourceTable entity = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
}
StopWatch w = new StopWatch();
T resourceToDelete = toResource(myResourceType, entity, false);
validateOkToDelete(deleteConflicts, entity);
preDelete(resourceToDelete, entity);
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theId.getResourceType(), theId);
notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
}
Date updateTime = new Date();
ResourceTable savedEntity = updateEntity(null, entity, updateTime, updateTime);
resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theId.getResourceType(), theId);
theRequestDetails.getRequestOperationCallback().resourceDeleted(resourceToDelete);
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity);
}
}
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequestDetails, resourceToDelete);
}
}
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true);
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
String severity = "information";
String code = "informational";
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
outcome.setOperationOutcome(oo);
return outcome;
}
@Override
public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
StopWatch w = new StopWatch();
DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal;
}
/**
* This method gets called by {@link #deleteByUrl(String, List, RequestDetails)} as well as by
* transaction processors
*/
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
Set<Long> resource = processMatchUrl(theUrl, myResourceType);
if (resource.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) {
throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
}
}
List<ResourceTable> deletedResources = new ArrayList<ResourceTable>();
for (Long pid : resource) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
deletedResources.add(entity);
validateOkToDelete(deleteConflicts, entity);
// Notify interceptors
IdDt idToDelete = entity.getIdDt();
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, idToDelete.getResourceType(), idToDelete);
notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
// Perform delete
Date updateTime = new Date();
updateEntity(null, entity, updateTime, updateTime);
// Notify JPA interceptors
T resourceToDelete = toResource(myResourceType, entity, false);
theRequestDetails.getRequestOperationCallback().resourceDeleted(resourceToDelete);
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity);
}
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theRequestDetails, resourceToDelete);
}
}
}
IBaseOperationOutcome oo;
if (deletedResources.isEmpty()) {
oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl);
String severity = "warning";
String code = "not-found";
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
} else {
oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", deletedResources.size(), w.getMillis());
String severity = "information";
String code = "informational";
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
}
ourLog.info("Processed delete on {} (matched {} resource(s)) in {}ms", new Object[] { theUrl, deletedResources.size(), w.getMillis() });
DeleteMethodOutcome retVal = new DeleteMethodOutcome();
retVal.setDeletedEntities(deletedResources);
retVal.setOperationOutcome(oo);
return retVal;
}
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) {
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
return outcome;
}
@PostConstruct
public void detectSearchDaoDisabled() {
if (mySearchDao != null && mySearchDao.isDisabled()) {
mySearchDao = null;
}
}
private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
if (isNotBlank(theIfNoneExist)) {
Set<Long> match = processMatchUrl(theIfNoneExist, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
throw new PreconditionFailedException(msg);
} else if (match.size() == 1) {
Long pid = match.iterator().next();
entity = myEntityManager.find(ResourceTable.class, pid);
return toMethodOutcome(entity, theResource).setCreated(false);
}
}
if (isNotBlank(theResource.getIdElement().getIdPart())) {
if (isValidPid(theResource.getIdElement())) {
throw new UnprocessableEntityException(
"This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID");
}
createForcedIdIfNeeded(entity, theResource.getIdElement());
if (entity.getForcedId() != null) {
try {
translateForcedIdToPid(getResourceName(), theResource.getIdElement().getIdPart());
throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "duplicateCreateForcedId", theResource.getIdElement().getIdPart()));
} catch (ResourceNotFoundException e) {
// good, this ID doesn't exist so we can create it
}
}
}
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theResource);
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
}
// Perform actual DB update
updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
theResource.setId(entity.getIdDt());
/*
* If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
* we'll manually increase the version. This is important because we want the updated version number
* to be reflected in the resource shared with interceptors
*/
if (!thePerformIndexing) {
incremenetId(theResource, entity, theResource.getIdElement());
}
// Notify JPA interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theResource);
theRequestDetails.getRequestOperationCallback().resourceCreated(theResource);
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceCreated(requestDetails, entity);
}
}
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceCreated(theRequestDetails, theResource);
}
}
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
}
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
ourLog.info(msg);
return outcome;
}
private void incremenetId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
IIdType idType = theResourceId;
String newVersion;
long newVersionLong;
if (idType == null || idType.getVersionIdPart() == null) {
newVersion = "1";
newVersionLong = 1;
} else {
newVersionLong = idType.getVersionIdPartAsLong() + 1;
newVersion = Long.toString(newVersionLong);
}
IIdType newId = theResourceId.withVersion(newVersion);
theResource.getIdElement().setValue(newId.getValue());
theSavedEntity.setVersion(newVersionLong);
}
private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource entity) {
List<TagDefinition> tags = toTagList(theMetaAdd);
//@formatter:off
for (TagDefinition nextDef : tags) {
boolean hasTag = false;
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
hasTag = true;
break;
}
}
if (!hasTag) {
entity.setHasTags(true);
TagDefinition def = getTag(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
BaseTag newEntity = entity.addTag(def);
myEntityManager.persist(newEntity);
}
}
//@formatter:on
myEntityManager.merge(entity);
}
private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource entity) {
List<TagDefinition> tags = toTagList(theMetaDel);
//@formatter:off
for (TagDefinition nextDef : tags) {
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
myEntityManager.remove(next);
entity.getTags().remove(next);
}
}
}
//@formatter:on
if (entity.getTags().isEmpty()) {
entity.setHasTags(false);
}
myEntityManager.merge(entity);
}
@Override
public TagList getAllResourceTags(RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
StopWatch w = new StopWatch();
TagList tags = super.getTags(myResourceType, null);
ourLog.info("Processed getTags on {} in {}ms", myResourceName, w.getMillisAndRestart());
return tags;
}
protected abstract List<Object> getIncludeValues(FhirTerser theTerser, Include theInclude, IBaseResource theResource, RuntimeResourceDefinition theResourceDef);
public String getResourceName() {
return myResourceName;
}
@Override
public Class<T> getResourceType() {
return myResourceType;
}
@Override
public TagList getTags(IIdType theResourceId, RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, null, theResourceId);
notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
StopWatch w = new StopWatch();
TagList retVal = super.getTags(myResourceType, theResourceId);
ourLog.info("Processed getTags on {} in {}ms", theResourceId, w.getMillisAndRestart());
return retVal;
}
@Override
public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.HISTORY_TYPE, requestDetails);
StopWatch w = new StopWatch();
IBundleProvider retVal = super.history(myResourceName, null, theSince, theUntil);
ourLog.info("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart());
return retVal;
}
@Override
public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails);
StopWatch w = new StopWatch();
IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
BaseHasResource entity = readEntity(id);
IBundleProvider retVal = super.history(myResourceName, entity.getId(), theSince, theUntil);
ourLog.info("Processed history on {} in {}ms", id, w.getMillisAndRestart());
return retVal;
}
@Override
public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theResourceId);
notifyInterceptors(RestOperationTypeEnum.META_ADD, requestDetails);
}
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theResourceId);
if (entity == null) {
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaAdd(theMetaAdd, entity);
} else {
doMetaAdd(theMetaAdd, latestVersion);
// Also update history entry
ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion());
doMetaAdd(theMetaAdd, history);
}
ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() });
@SuppressWarnings("unchecked")
MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequestDetails);
return retVal;
}
@Override
public <MT extends IBaseMetaType> MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theResourceId);
notifyInterceptors(RestOperationTypeEnum.META_DELETE, requestDetails);
}
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theResourceId);
if (entity == null) {
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaDelete(theMetaDel, entity);
} else {
doMetaDelete(theMetaDel, latestVersion);
// Also update history entry
ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion());
doMetaDelete(theMetaDel, history);
}
myEntityManager.flush();
ourLog.info("Processed metaDeleteOperation on {} in {}ms", new Object[] { theResourceId.getValue(), w.getMillisAndRestart() });
@SuppressWarnings("unchecked")
MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequestDetails);
return retVal;
}
@Override
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
}
Set<TagDefinition> tagDefs = new HashSet<TagDefinition>();
BaseHasResource entity = readEntity(theId);
for (BaseTag next : entity.getTags()) {
tagDefs.add(next.getTag());
}
MT retVal = toMetaDt(theType, tagDefs);
retVal.setLastUpdated(entity.getUpdatedDate());
retVal.setVersionId(Long.toString(entity.getVersion()));
return retVal;
}
@Override
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), null);
notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
}
String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)";
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
q.setParameter("res_type", myResourceName);
List<TagDefinition> tagDefinitions = q.getResultList();
MT retVal = toMetaDt(theType, tagDefinitions);
return retVal;
}
@Override
public DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
ResourceTable entityToUpdate = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart()) {
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
}
}
validateResourceType(entityToUpdate);
IBaseResource resourceToUpdate = toResource(entityToUpdate, false);
IBaseResource destination;
if (thePatchType == PatchTypeEnum.JSON_PATCH) {
destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
} else {
destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
}
@SuppressWarnings("unchecked")
T destinationCasted = (T) destination;
return update(destinationCasted, null, true, theRequestDetails);
}
@PostConstruct
public void postConstruct() {
RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType);
myResourceName = def.getName();
if (mySecondaryPrimaryKeyParamName != null) {
RuntimeSearchParam sp = getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
if (sp == null) {
throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]");
}
if (sp.getParamType() != RestSearchParameterTypeEnum.TOKEN) {
throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "] is not a token type, only token is supported");
}
}
}
/**
* Subclasses may override to provide behaviour. Invoked within a delete
* transaction with the resource that is about to be deleted.
*/
protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete) {
// nothing by default
}
/**
* May be overridden by subclasses to validate resources prior to storage
*
* @param theResource
* The resource that is about to be stored
*/
protected void preProcessResourceForStorage(T theResource) {
String type = getContext().getResourceDefinition(theResource).getName();
if (!getResourceName().equals(type)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName()));
}
if (theResource.getIdElement().hasIdPart()) {
if (!theResource.getIdElement().isIdPartValid()) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart()));
}
}
/*
* Replace absolute references with relative ones if configured to do so
*/
if (getConfig().getTreatBaseUrlsAsLocal().isEmpty() == false) {
FhirTerser t = getContext().newTerser();
List<ResourceReferenceInfo> refs = t.getAllResourceReferences(theResource);
for (ResourceReferenceInfo nextRef : refs) {
IIdType refId = nextRef.getResourceReference().getReferenceElement();
if (refId != null && refId.hasBaseUrl()) {
if (getConfig().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) {
IIdType newRefId = refId.toUnqualified();
nextRef.getResourceReference().setReference(newRefId.getValue());
}
}
}
}
}
@Override
public Set<Long> processMatchUrl(String theMatchUrl) {
return processMatchUrl(theMatchUrl, getResourceType());
}
@Override
public T read(IIdType theId) {
return read(theId, null);
}
@Override
public T read(IIdType theId, RequestDetails theRequestDetails) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
RestOperationTypeEnum operationType = theId.hasVersionIdPart() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
notifyInterceptors(operationType, requestDetails);
}
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theId);
validateResourceType(entity);
T retVal = toResource(myResourceType, entity, false);
IPrimitiveType<Date> deleted;
if (retVal instanceof IResource) {
deleted = ResourceMetadataKeyEnum.DELETED_AT.get((IResource) retVal);
} else {
deleted = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) retVal);
}
if (deleted != null && !deleted.isEmpty()) {
throw new ResourceGoneException("Resource was deleted at " + deleted.getValueAsString());
}
ourLog.info("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal;
}
@Override
public BaseHasResource readEntity(IIdType theId) {
boolean checkForForcedId = true;
BaseHasResource entity = readEntity(theId, checkForForcedId);
return entity;
}
@Override
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
Long pid = translateForcedIdToPid(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
if (theId.hasVersionIdPart()) {
if (theId.isVersionIdPartValidLong() == false) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
}
if (entity.getVersion() != theId.getVersionIdPartAsLong().longValue()) {
entity = null;
}
}
if (entity == null) {
if (theId.hasVersionIdPart()) {
TypedQuery<ResourceHistoryTable> q = myEntityManager
.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", pid);
q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", theId.getVersionIdPartAsLong());
try {
entity = q.getSingleResult();
} catch (NoResultException e) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
}
}
}
validateResourceType(entity);
if (theCheckForForcedId) {
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
}
return entity;
}
protected ResourceTable readEntityLatestVersion(IIdType theId) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, translateForcedIdToPid(getResourceName(), theId.getIdPart()));
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
return entity;
}
@Override
public void reindex(T theResource, ResourceTable theEntity) {
updateEntity(theResource, theEntity, null, true, false, theEntity.getUpdatedDate(), true, false);
}
@Override
public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) {
removeTag(theId, theTagType, theScheme, theTerm, null);
}
@Override
public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
notifyInterceptors(RestOperationTypeEnum.DELETE_TAGS, requestDetails);
}
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theId);
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
//@formatter:off
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) &&
ObjectUtil.equals(next.getTag().getSystem(), theScheme) &&
ObjectUtil.equals(next.getTag().getCode(), theTerm)) {
myEntityManager.remove(next);
entity.getTags().remove(next);
}
}
//@formatter:on
if (entity.getTags().isEmpty()) {
entity.setHasTags(false);
}
myEntityManager.merge(entity);
ourLog.info("Processed remove tag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId.getValue(), w.getMillisAndRestart() });
}
@Transactional(propagation=Propagation.SUPPORTS)
@Override
public IBundleProvider search(final SearchParameterMap theParams) {
return search(theParams, null);
}
@Transactional(propagation=Propagation.SUPPORTS)
@Override
public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails) {
// Notify interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), getResourceName(), null);
notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails);
if (theRequestDetails.isSubRequest()) {
theParams.setLoadSynchronous(true);
theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction());
}
}
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName());
}
@Override
public Set<Long> searchForIds(SearchParameterMap theParams) {
SearchBuilder builder = newSearchBuilder();
builder.setType(getResourceType(), getResourceName());
// FIXME: fail if too many results
HashSet<Long> retVal = new HashSet<Long>();
Iterator<Long> iter = builder.createQuery(theParams);
while (iter.hasNext()) {
retVal.add(iter.next());
}
return retVal;
}
@SuppressWarnings("unchecked")
@Required
public void setResourceType(Class<? extends IBaseResource> theTableType) {
myResourceType = (Class<T>) theTableType;
}
/**
* If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to share the same value.
*/
public void setSecondaryPrimaryKeyParamName(String theSecondaryPrimaryKeyParamName) {
mySecondaryPrimaryKeyParamName = theSecondaryPrimaryKeyParamName;
}
protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) {
MT retVal;
try {
retVal = theType.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failed to instantiate " + theType.getName(), e);
}
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
case PROFILE:
retVal.addProfile(next.getCode());
break;
case SECURITY_LABEL:
retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
case TAG:
retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
}
}
return retVal;
}
private DaoMethodOutcome toMethodOutcome(final BaseHasResource theEntity, IBaseResource theResource) {
DaoMethodOutcome outcome = new DaoMethodOutcome();
IIdType id = theEntity.getIdDt();
if (getContext().getVersion().getVersion().isRi()) {
id = new IdType(id.getValue());
}
outcome.setId(id);
outcome.setResource(theResource);
if (theResource != null) {
theResource.setId(id);
if (theResource instanceof IResource) {
ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated());
} else {
IBaseMetaType meta = ((IAnyResource) theResource).getMeta();
meta.setLastUpdated(theEntity.getUpdatedDate());
}
}
return outcome;
}
private DaoMethodOutcome toMethodOutcome(final ResourceTable theEntity, IBaseResource theResource) {
DaoMethodOutcome retVal = toMethodOutcome((BaseHasResource) theEntity, theResource);
retVal.setEntity(theEntity);
return retVal;
}
private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) {
ArrayList<TagDefinition> retVal = new ArrayList<TagDefinition>();
for (IBaseCoding next : theMeta.getTag()) {
retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()));
}
for (IBaseCoding next : theMeta.getSecurity()) {
retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()));
}
for (IPrimitiveType<String> next : theMeta.getProfile()) {
retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null));
}
return retVal;
}
@Transactional(propagation=Propagation.SUPPORTS)
@Override
public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) {
if (theSource == null || theSource.isEmpty()) {
return;
}
Map<String, RuntimeSearchParam> searchParams = mySerarchParamRegistry.getActiveSearchParams(getResourceName());
Set<String> paramNames = theSource.keySet();
for (String nextParamName : paramNames) {
QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
if (param == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<String>(searchParams.keySet()));
throw new InvalidRequestException(msg);
}
// Should not be null since the check above would have caught it
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceName);
RuntimeSearchParam paramDef = getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
for (String nextValue : theSource.get(nextParamName)) {
if (isNotBlank(nextValue)) {
QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifiedParamName.getWholeQualifier(), nextValue);
List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam);
IQueryParameterAnd<?> parsedParam = MethodUtil.parseQueryParams(getContext(), paramDef, nextParamName, paramList);
theTarget.add(qualifiedParamName.getParamName(), parsedParam);
}
}
}
}
@Override
public DaoMethodOutcome update(T theResource) {
return update(theResource, null, null);
}
@Override
public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) {
return update(theResource, null, theRequestDetails);
}
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl) {
return update(theResource, theMatchUrl, null);
}
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
}
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
final ResourceTable entity;
IIdType resourceId;
if (isNotBlank(theMatchUrl)) {
Set<Long> match = processMatchUrl(theMatchUrl, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
throw new PreconditionFailedException(msg);
} else if (match.size() == 1) {
Long pid = match.iterator().next();
entity = myEntityManager.find(ResourceTable.class, pid);
resourceId = entity.getIdDt();
} else {
return create(theResource, null, thePerformIndexing, theRequestDetails);
}
} else {
/*
* Note: resourcdeId will not be null or empty here, because we check it and reject requests in BaseOutcomeReturningMethodBindingWithResourceParam
*/
resourceId = theResource.getIdElement();
try {
entity = readEntityLatestVersion(resourceId);
} catch (ResourceNotFoundException e) {
if (resourceId.isIdPartValidLong()) {
throw new InvalidRequestException(
getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()));
}
return doCreate(theResource, null, thePerformIndexing, new Date(), theRequestDetails);
}
}
if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) {
throw new ResourceVersionConflictException("Trying to update " + resourceId + " but this is not the current version");
}
if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
throw new UnprocessableEntityException(
"Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
}
// Notify interceptors
ActionRequestDetails requestDetails = null;
if (theRequestDetails != null) {
requestDetails = new ActionRequestDetails(theRequestDetails, theResource, getResourceName(), resourceId);
notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails);
}
// Perform update
ResourceTable savedEntity = updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
/*
* If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
* we'll manually increase the version. This is important because we want the updated version number
* to be reflected in the resource shared with interceptors
*/
if (!thePerformIndexing) {
if (resourceId.hasVersionIdPart() == false) {
resourceId = resourceId.withVersion(Long.toString(savedEntity.getVersion()));
}
incremenetId(theResource, savedEntity, resourceId);
}
// Notify interceptors
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceUpdated(requestDetails, entity);
}
}
}
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
}
}
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
}
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
ourLog.info(msg);
return outcome;
}
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, true, theRequestDetails);
}
private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) {
if (entity.getForcedId() != null) {
if (theId.isIdPartValidLong()) {
// This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
// as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer
// to the
// forced ID)
throw new ResourceNotFoundException(theId);
}
}
}
protected void validateOkToDelete(List<DeleteConflict> theDeleteConflicts, ResourceTable theEntity) {
TypedQuery<ResourceLink> query = myEntityManager.createQuery("SELECT l FROM ResourceLink l WHERE l.myTargetResourcePid = :target_pid", ResourceLink.class);
query.setParameter("target_pid", theEntity.getId());
query.setMaxResults(1);
List<ResourceLink> resultList = query.getResultList();
if (resultList.isEmpty()) {
return;
}
ResourceLink link = resultList.get(0);
IdDt targetId = theEntity.getIdDt();
IdDt sourceId = link.getSourceResource().getIdDt();
String sourcePath = link.getSourcePath();
theDeleteConflicts.add(new DeleteConflict(sourceId, sourcePath, targetId));
}
private void validateResourceType(BaseHasResource entity) {
validateResourceType(entity, myResourceName);
}
private void validateResourceTypeAndThrowIllegalArgumentException(IIdType theId) {
if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
throw new IllegalArgumentException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
}
}
}