package edu.gatech.i3l.fhir.jpa.dao; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContextType; import javax.persistence.Tuple; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.validation.ConstraintViolation; import javax.validation.Validator; import net.vidageek.mirror.dsl.Mirror; import org.apache.commons.lang3.reflect.FieldUtils; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.hibernate.envers.query.AuditEntity; import org.hibernate.envers.query.AuditQuery; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue; import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum; import ca.uhn.fhir.model.dstu2.valueset.IssueTypeEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParserErrorHandler; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import edu.gatech.i3l.fhir.jpa.entity.BaseResourceEntity; import edu.gatech.i3l.fhir.jpa.entity.IResourceEntity; import edu.gatech.i3l.fhir.jpa.query.PredicateBuilder; import edu.gatech.i3l.fhir.jpa.query.QueryHelper; import edu.gatech.i3l.fhir.jpa.util.DaoUtils; import edu.gatech.i3l.fhir.jpa.util.StopWatch; /** * This class serves as Template with commmon dao functions that are meant to be extended by subclasses. * @author Ismael Sarmento */ @Transactional(propagation = Propagation.REQUIRED) public abstract class BaseFhirResourceDao<T extends IResource> implements IFhirResourceDao<T>{ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseFhirResourceDao.class); @PersistenceContext(type = PersistenceContextType.TRANSACTION) private EntityManager myEntityManager; private BaseFhirDao baseFhirDao; private boolean createBundle; @Autowired private PlatformTransactionManager myPlatformTransactionManager; private Class<T> myResourceType; private Class<? extends IResourceEntity> myResourceEntity; private FhirContext myContext; private QueryHelper myQueryHelper ; private Validator myBeanValidator; private boolean myValidateBean; public abstract PredicateBuilder getPredicateBuilder(); public BaseFhirResourceDao() { super(); } @PostConstruct public void setQueryHelper(){ if(myQueryHelper == null){ myQueryHelper = new QueryHelper( this.myEntityManager, this.myResourceEntity, this.myResourceType, this.myContext, this.baseFhirDao); myQueryHelper.setPredicateBuilder(getPredicateBuilder()); } } public Validator getBeanValidator() { return myBeanValidator; } public void setBeanValidator(Validator theBeanValidator) { this.myBeanValidator = theBeanValidator; } public boolean isValidateBean() { return myValidateBean; } public void setValidateBean(boolean theValidateBean) { this.myValidateBean = theValidateBean; } @Override public Class<T> getResourceType() { return myResourceType; } @SuppressWarnings("unchecked") @Required public void setResourceType(Class<? extends IResource> theTableType) { myResourceType = (Class<T>) theTableType; } public Class<? extends IResourceEntity> getResourceEntity() { return myResourceEntity; } public void setResourceEntity(Class<? extends IResourceEntity> theResourceEntity) { this.myResourceEntity = theResourceEntity; } public void setContext(FhirContext context){ this.myContext = context; } public FhirContext getContext(){ return this.myContext; } public EntityManager getEntityManager() { return myEntityManager; } public void setEntityManager(EntityManager myEntityManager) { this.myEntityManager = myEntityManager; } public QueryHelper getQueryHelper() { return myQueryHelper; } public void setQueryHelper(QueryHelper myQueryHelper) { this.myQueryHelper = myQueryHelper; } @Override public DaoMethodOutcome create(final T theResource) { return create(theResource, null, true); } @Override public DaoMethodOutcome create(final T theResource, String theIfNoneExist) { return create(theResource, theIfNoneExist, true); } @Override public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing) { // if (isNotBlank(theResource.getId().getIdPart())) { // throw new InvalidRequestException(baseFhirDao.getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getId().getIdPart())); // } return doCreate(theResource, theIfNoneExist, thePerformIndexing); } private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing) { StopWatch w = new StopWatch(); BaseResourceEntity entity = (BaseResourceEntity) new Mirror().on(myResourceEntity).invoke().constructor().withoutArgs(); entity.constructEntityFromResource(theResource); if(myValidateBean){ Set<ConstraintViolation<BaseResourceEntity>> violations = myBeanValidator.validate(entity); if(!violations.isEmpty()){ OperationOutcome oo = new OperationOutcome(); for (ConstraintViolation<BaseResourceEntity> violation : violations) { // oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.PROCESSING_FAILURE).setDetails(violation.getPropertyPath()+" "+ violation.getMessage()); oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.PROCESSING_FAILURE).setDetails((new CodeableConceptDt()).setText(violation.getPropertyPath()+" "+ violation.getMessage())); } throw new UnprocessableEntityException(myContext, oo); } } // if (isNotBlank(theIfNoneExist)) { // Set<Long> match = DaoUtils.processMatchUrl(theIfNoneExist, myResourceType, myContext, getBaseFhirDao().getDao(myResourceType)); // if (match.size() > 1) { // String msg = baseFhirDao.getContext().getLocalizer().getMessage(BaseFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); // throw new PreconditionFailedException(msg); // } else if (match.size() == 1) { // Long pid = match.iterator().next(); // entity = myEntityManager.find(myResourceEntity, pid); // return toMethodOutcome(entity, theResource).setCreated(false); // } // } baseFhirDao.updateEntity(theResource, entity, false, null, thePerformIndexing, true); DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); baseFhirDao.notifyWriteCompleted(); ourLog.info("Processed create on {} in {}ms", myResourceType, w.getMillisAndRestart()); return outcome; } private DaoMethodOutcome toMethodOutcome(final BaseResourceEntity entity, IResource theResource) { DaoMethodOutcome outcome = new DaoMethodOutcome(); outcome.setId(new IdDt(entity.getId())); outcome.setEntity(entity); outcome.setResource(theResource); if (theResource != null) { theResource.setId(new IdDt(entity.getId())); } return outcome; } @SuppressWarnings("unchecked") @Override public T read(IIdType theId) { StopWatch w = new StopWatch(); BaseResourceEntity entity = (BaseResourceEntity) readEntity(theId); T retVal = null; if( entity != null){ retVal = (T) entity.getRelatedResource(); InstantDt deleted = ResourceMetadataKeyEnum.DELETED_AT.get(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 BaseResourceEntity readEntity(IIdType theId) { BaseResourceEntity entity = readEntity(theId, false); return entity; } @Override public BaseResourceEntity readEntity(IIdType theId, boolean theCheckForForcedId) { Long pid = theId.getIdPartAsLong(); BaseResourceEntity entity = (BaseResourceEntity) myEntityManager.find(getResourceEntity(), pid); // if (theId.hasVersionIdPart()) { //TODO new capability: Read resource from history, id-format = 'id/_history/version' // if (entity.getVersion() != theId.getVersionIdPartAsLong()) { // entity = null; // } // } // // if (entity == null) { // if (theId.hasVersionIdPart()) { // TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery( // "SELECT t from ); // entity = q.getSingleResult(); // } // if (entity == null) { // throw new ResourceNotFoundException(theId); // } // } return entity; } @Override public IBundleProvider search(Map<String, IQueryParameterType> theParams) { SearchParameterMap map = new SearchParameterMap(); for (Entry<String, IQueryParameterType> nextEntry : theParams.entrySet()) { map.add(nextEntry.getKey(), (nextEntry.getValue())); } return search(map); } public IBundleProvider search(String theParameterName, IQueryParameterType theValue) { return search(Collections.singletonMap(theParameterName, theValue)); } @Override public Set<Long> searchForIds(Map<String, IQueryParameterType> theParams) { SearchParameterMap map = new SearchParameterMap(); for (Entry<String, IQueryParameterType> nextEntry : theParams.entrySet()) { map.add(nextEntry.getKey(), (nextEntry.getValue())); } return searchForIdsWithAndOr(map); } @Override public Set<Long> searchForIds(String theParameterName, IQueryParameterType theValue) { return searchForIds(Collections.singletonMap(theParameterName, theValue)); } /** * Search for the <b>current revisions</b> of entity, according to the map with filters. * In order to add historic data to the search, it is suggested to override the method * {@link BaseFhirResourceDao#loadResourcesByPid(Collection, List, BundleEntrySearchModeEnum)}. */ @Override public IBundleProvider search(final SearchParameterMap theParams) { StopWatch w = new StopWatch(); final InstantDt now = InstantDt.withCurrentTime(); System.out.println("theParams:"+theParams.toString()); Set<Long> loadPids; if (theParams.isEmpty()) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> criteria = builder.createQuery(Long.class); Root<? extends IResourceEntity> from = criteria.from(getResourceEntity()); criteria.select(from.get("id").as(Long.class)); Predicate addPredicate = myQueryHelper.getPredicateBuilder().addCommonPredicate(builder, from); if(addPredicate != null) criteria.where(addPredicate); criteria.orderBy(builder.asc(from.get("id").as(Long.class))); List<Long> resultList = myEntityManager.createQuery(criteria).getResultList(); loadPids = new HashSet<Long>(resultList); } else { loadPids = searchForIdsWithAndOr(theParams); if (loadPids.isEmpty()) { return new SimpleBundleProvider(); } } final List<Long> pids; if (theParams.getSort() != null && isNotBlank(theParams.getSort().getParamName())) { List<Order> orders = new ArrayList<Order>(); List<Predicate> predicates = new ArrayList<Predicate>(); CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Tuple> cq = builder.createTupleQuery(); Root<? extends IResourceEntity> from = cq.from(getResourceEntity()); predicates.add(from.get("id").in(loadPids)); createSort(builder, from, theParams.getSort(), orders); if (orders.size() > 0) { Set<Long> originalPids = loadPids; loadPids = new LinkedHashSet<Long>(); cq.multiselect(from.get("id").as(Long.class)); cq.where(predicates.toArray(new Predicate[0])); cq.orderBy(orders); TypedQuery<Tuple> query = myEntityManager.createQuery(cq); for (Tuple next : query.getResultList()) { loadPids.add(next.get(0, Long.class)); } ourLog.info("Sort PID order is now: {}", loadPids); pids = new ArrayList<Long>(loadPids); // Any ressources which weren't matched by the sort get added to the bottom for (Long next : originalPids) { if (loadPids.contains(next) == false) { pids.add(next); } } } else { pids = new ArrayList<Long>(loadPids); } } else { pids = new ArrayList<Long>(loadPids); } IBundleProvider retVal = new IBundleProvider() { @Override public InstantDt getPublished() { return now; } @Override public List<IBaseResource> getResources(final int theFromIndex, final int theToIndex) { TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); return template.execute(new TransactionCallback<List<IBaseResource>>() { @Override public List<IBaseResource> doInTransaction(TransactionStatus theStatus) { List<Long> pidsSubList = pids.subList(theFromIndex, theToIndex); // Execute the query and make sure we return distinct results List<IBaseResource> retVal = new ArrayList<IBaseResource>(); loadResourcesByPid(pidsSubList, retVal, BundleEntrySearchModeEnum.MATCH); /* * Load _include resources - Note that _revincludes are handled differently than _include ones, as they are counted towards the total count and paged, so they are loaded * outside the bundle provider */ if (theParams.getIncludes() != null && theParams.getIncludes().isEmpty() == false) { Set<IIdType> previouslyLoadedPids = new HashSet<IIdType>(); for (IBaseResource next : retVal) { previouslyLoadedPids.add(next.getIdElement().toUnqualifiedVersionless()); } Set<IdDt> includePids = new HashSet<IdDt>(); do { includePids.clear(); FhirTerser t = baseFhirDao.getContext().newTerser(); for (Include next : theParams.getIncludes()) { for (IBaseResource nextResource : retVal) { RuntimeResourceDefinition def = baseFhirDao.getContext().getResourceDefinition(nextResource); List<Object> values = null; switch (baseFhirDao.getContext().getVersion().getVersion()) { case DSTU2: if ("*".equals(next.getValue())) { values = new ArrayList<Object>(); values.addAll(t.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class)); } else if (next.getValue().startsWith(def.getName() + ":")) { values = new ArrayList<Object>(); RuntimeSearchParam sp = def.getSearchParam(next.getValue().substring(next.getValue().indexOf(':')+1)); for (String nextPath : sp.getPathsSplit()) { values.addAll(t.getValues(nextResource, nextPath)); } } else { values = Collections.emptyList(); } break; default: break; } if(values == null) throw new IllegalStateException("Support for Search not provided for Version: " + baseFhirDao.getContext().getVersion().getVersion()); for (Object object : values) { if (object == null) { continue; } if (!(object instanceof BaseResourceReferenceDt)) { throw new InvalidRequestException("Path '" + next.getValue() + "' produced non ResourceReferenceDt value: " + object.getClass()); } BaseResourceReferenceDt rr = (BaseResourceReferenceDt) object; if (rr.getReference().isEmpty()) { continue; } if (rr.getReference().isLocal()) { continue; } IdDt nextId = rr.getReference().toUnqualified(); if (!previouslyLoadedPids.contains(nextId)) { includePids.add(nextId); previouslyLoadedPids.add(nextId); } } } } addResourcesAsIncludesById(retVal, includePids); } while (includePids.size() > 0 && previouslyLoadedPids.size() < baseFhirDao.getConfig().getIncludeLimit()); if (previouslyLoadedPids.size() >= baseFhirDao.getConfig().getIncludeLimit()) { OperationOutcome oo = new OperationOutcome(); oo.addIssue().setSeverity(IssueSeverityEnum.WARNING) .setDetails("Not all _include resources were actually included as the request surpassed the limit of " + baseFhirDao.getConfig().getIncludeLimit() + " resources"); retVal.add(0, oo); } } return retVal; } }); } @Override public Integer preferredPageSize() { return theParams.getCount(); } @Override public int size() { return pids.size(); } }; ourLog.info("Processed search for {} on {} in {}ms", new Object[] { getResourceType(), theParams, w.getMillisAndRestart() }); return retVal; } private void addResourcesAsIncludesById(List<IBaseResource> theListToPopulate, Set<? extends IIdType> includePids) { if (!includePids.isEmpty()) { ourLog.info("Loading {} included resources", includePids.size()); Set<Long> pids = new HashSet<Long>(); for (IIdType next : includePids) { if (next.isIdPartValidLong()) { pids.add(next.getIdPartAsLong()); } } if (pids.isEmpty()) { return; } Class<?> theResourceEntity = null; try { String fieldName = getResourceEntity().newInstance().translateSearchParam(includePids.iterator().next().getResourceType().toLowerCase()); Field includeField = FieldUtils.getField(getResourceEntity(), fieldName, true); theResourceEntity = includeField.getType(); } catch (SecurityException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } CriteriaBuilder builder = baseFhirDao.getEntityManager().getCriteriaBuilder(); CriteriaQuery<?> cq = builder.createQuery(theResourceEntity); Root<?> from = cq.from(theResourceEntity); cq.where(from.get("id").in(pids)); TypedQuery<?> q = baseFhirDao.getEntityManager().createQuery(cq); for (Object next : q.getResultList()) { IResource resource = (IResource) ((IResourceEntity)next).getRelatedResource(); theListToPopulate.add(resource); } for (IBaseResource next : theListToPopulate) { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) next, BundleEntrySearchModeEnum.INCLUDE); } } } // // // //@Override //public void registerDaoListener(IDaoListener theListener) { //// TODO Auto-generated method stub // //} // //@Override //public DaoMethodOutcome delete(IIdType theId) { //StopWatch w = new StopWatch(); ////final BaseResourceEntity entity = readEntityLatestVersion(theId); ////if (theId.hasVersionIdPart() && theId.getVersionIdPartAsLong().longValue() != entity.getVersion()) { //// throw new InvalidRequestException("Trying to update " + theId + " but this is not the current version"); ////} //// ////BaseResourceEntity savedEntity = baseFhirDao.updateEntity(null, entity, true, new Date()); //BaseResourceEntity reference = (BaseResourceEntity) myEntityManager.getReference(myResourceEntity, theId.getIdPartAsLong()); //myEntityManager.remove(reference); // //myBaseFhirDao.notifyWriteCompleted(); // //ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); //return toMethodOutcome(reference, null); //} // //@Override //public DaoMethodOutcome deleteByUrl(String theString) { //// TODO Auto-generated method stub //return null; //} // //@Override //public TagList getAllResourceTags() { //// TODO Auto-generated method stub //return null; //} // //@Override //public IBundleProvider history(Date theSince) { //return null; //} // //@Override //public IBundleProvider history(Long theId, Date theSince) { //final InstantDt end = DaoUtils.createHistoryToTimestamp(); //int limit = 10000; //AuditReader auditReader = AuditReaderFactory.get(myEntityManager); //AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); //if(theId != null){ // query.add(AuditEntity.id().eq(theId)); //} //if(theSince != null){ // query.add(AuditEntity.revisionNumber().ge(auditReader.getRevisionNumberForDate(theSince))); //} //query.addProjection(AuditEntity.revisionNumber()); //query.addOrder(AuditEntity.revisionNumber().asc()); //final List<Number> revs = query.getResultList(); // //return new IBundleProvider() { // // @Override // public int size() { // return revs.size(); // } // // @Override // public Integer preferredPageSize() { // return null; // } // // @Override // public List<IBaseResource> getResources(final int theFromIndex,final int theToIndex) { // //final StopWatch timer = new StopWatch(); // TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); // return template.execute(new TransactionCallback<List<IBaseResource>>(){ // // @Override // public List<IBaseResource> doInTransaction(TransactionStatus status) { // ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>(); // if(!revs.isEmpty()){ // int limit = theToIndex - theFromIndex; // AuditReader auditReader = AuditReaderFactory.get(myEntityManager); // AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); // query.add(AuditEntity.revisionNumber().in(revs.toArray(new Number[0]))); // query.addOrder(AuditEntity.revisionNumber().asc()); // List<? extends IResourceEntity> resEntities = query.getResultList(); // if (resEntities.size() > limit) { // resEntities = resEntities.subList(0, limit); // } // for (int i = 0; i < resEntities.size(); i++) { // BaseResourceEntity entity = (BaseResourceEntity) resEntities.get(i);//WARNING works only for instances of BaseResourceEntity // entity.setVersion(revs.get(i).longValue()); // IResource resource = entity.getRelatedResource(); // retVal.add(resource); // // } // } // return retVal; // } // }); // } // // @Override // public InstantDt getPublished() { // return end; // } //}; //} // //@Override //public IBundleProvider history(IIdType theId, Date theSince) { //return history(theId.getIdPartAsLong(), theSince);//TODO add unit test //} // //private AuditQuery getAuditQuery(Long theId, Date theSince){ //int limit = 10000; //AuditReader auditReader = AuditReaderFactory.get(myEntityManager); //AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); ////query.add(AuditEntity.revisionNumber().in(values)) //return query; //} // //@Override //public DaoMethodOutcome update(T theResource) { //return update(theResource, null); //} // //@Override //public DaoMethodOutcome update(T theResource, String theMatchUrl) { //return update(theResource, theMatchUrl, true); //} /** * @hapi {@link BaseHapiFhirResourceDao#createSort(CriteriaBuilder, Root, SortSpec, List)} * @param theBuilder * @param from * @param theSort * @param theOrders */ protected void createSort(CriteriaBuilder theBuilder, Root<? extends IResourceEntity> from, SortSpec theSort, List<Order> theOrders) { if (theSort == null || isBlank(theSort.getParamName())) { return; } if ("_id".equals(theSort.getParamName())) { if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { theOrders.add(theBuilder.asc(from.get("id"))); } else { theOrders.add(theBuilder.desc(from.get("id"))); } createSort(theBuilder, from, theSort.getChain(), theOrders); return; } RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType); RuntimeSearchParam param = resourceDef.getSearchParam(theSort.getParamName()); if (param == null) { throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'"); } //TODO new capability: Sort by any type of parameters; currently it's supported only sorting by Id // String joinAttrName; // String sortAttrName; // // switch (param.getParamType()) { // case STRING: // joinAttrName = "myParamsString"; // sortAttrName = "myValueExact"; // break; // case DATE: // joinAttrName = "myParamsDate"; // sortAttrName = "myValueLow"; // break; // case REFERENCE: // joinAttrName = "myResourceLinks"; // sortAttrName = "myTargetResourcePid"; // break; // default: // throw new NotImplementedException("This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName()); // } // // From<?, ?> stringJoin = theFrom.join(joinAttrName, JoinType.INNER); // // if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { // theOrders.add(theBuilder.asc(stringJoin.get(sortAttrName))); // } else { // theOrders.add(theBuilder.desc(stringJoin.get(sortAttrName))); // } createSort(theBuilder, from, theSort.getChain(), theOrders); } @Override public Set<Long> searchForIdsWithAndOr(SearchParameterMap theParams) { SearchParameterMap params = theParams; if (params == null) { params = new SearchParameterMap(); } RuntimeResourceDefinition resourceDef = baseFhirDao.getContext().getResourceDefinition(myResourceType); Set<Long> pids = new HashSet<Long>(); for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : params.entrySet()) { String nextParamName = nextParamEntry.getKey(); if (nextParamName.equals("_id")) { if (nextParamEntry.getValue().isEmpty()) { continue; } else if (nextParamEntry.getValue().size() > 1) { throw new InvalidRequestException("AND queries not supported for _id (Multiple instances of this param found)"); } else { Set<Long> joinPids = new HashSet<Long>(); List<? extends IQueryParameterType> nextValue = nextParamEntry.getValue().get(0); if (nextValue == null || nextValue.size() == 0) { continue; } else { for (IQueryParameterType next : nextValue) { String value = next.getValueAsQueryToken(myContext); IdDt valueId = new IdDt(value); try { if (DaoUtils.isValidPid(valueId)) { long valueLong = valueId.getIdPartAsLong(); joinPids.add(valueLong); } } catch (ResourceNotFoundException e) { // This isn't an error, just means no result found } } if (joinPids.isEmpty()) { continue; } } pids = myQueryHelper.searchById(pids, joinPids); if (pids.isEmpty()) { return new HashSet<Long>(); } // if (pids.isEmpty()) { // pids.addAll(joinPids); // } else { // pids.retainAll(joinPids); // } } // } // else if (nextParamName.equals("_language")) { // // pids = addPredicateLanguage(pids, nextParamEntry.getValue()); } else { RuntimeSearchParam nextParamDef = resourceDef.getSearchParam(nextParamName); if (nextParamDef != null) { switch (nextParamDef.getParamType()) { case DATE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByDate(nextParamName, pids, nextAnd); } break; case QUANTITY: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByQuantity(nextParamName, pids, nextAnd); } break; case REFERENCE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByReference(nextParamName, pids, nextAnd); } break; case STRING: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByString(nextParamName, pids, nextAnd); } break; case TOKEN: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByToken(nextParamName, pids, nextAnd); } break; case NUMBER: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByNumber(nextParamName, pids, nextAnd); } break; case COMPOSITE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { pids = myQueryHelper.searchByComposite(nextParamDef, pids, nextAnd); } break; default: break; } if (pids.isEmpty()) { return new HashSet<Long>(); } } } } return pids; } public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, BundleEntrySearchModeEnum theBundleEntryStatus) { if (theIncludePids.isEmpty()) { return; } Map<Long, Integer> position = new HashMap<Long, Integer>(); for (Long next : theIncludePids) { position.put(next, theResourceListToPopulate.size()); theResourceListToPopulate.add(null); } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); @SuppressWarnings("unchecked") CriteriaQuery<IResourceEntity> cq = (CriteriaQuery<IResourceEntity>) builder.createQuery(getResourceEntity()); Root<? extends IResourceEntity> from = cq.from(getResourceEntity()); cq.select(from); cq.where(from.get("id").in(theIncludePids)); TypedQuery<IResourceEntity> q = myEntityManager.createQuery(cq); for (IResourceEntity entity : q.getResultList()) { //Class<? extends IBaseResource> resourceType = baseFhirDao.getContext().getResourceDefinition(next.getResourceType()).getImplementingClass(); IResource resource = entity.getRelatedResource(); Integer index = position.get(entity.getId()); if (index == null) { ourLog.warn("Got back unexpected resource PID {}", entity.getId()); continue; } ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, theBundleEntryStatus); theResourceListToPopulate.set(index, resource); } } public BaseFhirDao getBaseFhirDao() { return baseFhirDao; } public void setBaseFhirDao(BaseFhirDao baseFhirDao) { this.baseFhirDao = baseFhirDao; } @Override public DaoMethodOutcome delete(IIdType theId) { StopWatch w = new StopWatch(); BaseResourceEntity reference = (BaseResourceEntity) baseFhirDao.getEntityManager().getReference(myResourceEntity, theId.getIdPartAsLong()); baseFhirDao.getEntityManager().remove(reference); baseFhirDao.notifyWriteCompleted(); ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); return toMethodOutcome(reference, null); } @Override public DaoMethodOutcome deleteByUrl(String theString) { // TODO new capability: delete by url should be supported in a later version return null; } @Override public IBundleProvider history(Date theSince) { return null;//history(null, theSince); } @Override public IBundleProvider history(Long theId, Date theSince) { final InstantDt end = DaoUtils.createHistoryToTimestamp(); int limit = 10000; AuditReader auditReader = AuditReaderFactory.get(myEntityManager); AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); if(theId != null){ query.add(AuditEntity.id().eq(theId)); } if(theSince != null){ query.add(AuditEntity.revisionNumber().ge(auditReader.getRevisionNumberForDate(theSince))); } query.addProjection(AuditEntity.revisionNumber()); query.addOrder(AuditEntity.revisionNumber().asc()); final List<Number> revs = query.getResultList(); return new IBundleProvider() { @Override public int size() { return revs.size(); } @Override public Integer preferredPageSize() { return null; } @Override public List<IBaseResource> getResources(final int theFromIndex,final int theToIndex) { //final StopWatch timer = new StopWatch(); TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); return template.execute(new TransactionCallback<List<IBaseResource>>(){ @Override public List<IBaseResource> doInTransaction(TransactionStatus status) { ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>(); if(!revs.isEmpty()){ int limit = theToIndex - theFromIndex; AuditReader auditReader = AuditReaderFactory.get(myEntityManager); AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); query.add(AuditEntity.revisionNumber().in(revs.toArray(new Number[0]))); query.addOrder(AuditEntity.revisionNumber().asc()); List<? extends IResourceEntity> resEntities = query.getResultList(); if (resEntities.size() > limit) { resEntities = resEntities.subList(0, limit); } for (int i = 0; i < resEntities.size(); i++) { BaseResourceEntity entity = (BaseResourceEntity) resEntities.get(i);//WARNING works only for instances of BaseResourceEntity entity.setVersion(revs.get(i).longValue()); IResource resource = entity.getRelatedResource(); retVal.add(resource); } } return retVal; } }); } @Override public InstantDt getPublished() { return end; } }; } @Override public IBundleProvider history(IIdType theId, Date theSince) { return history(theId.getIdPartAsLong(), theSince); } private AuditQuery getAuditQuery(Long theId, Date theSince){ int limit = 10000; AuditReader auditReader = AuditReaderFactory.get(myEntityManager); AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(myResourceEntity, true, false).setMaxResults(limit); // query.add(AuditEntity.revisionNumber().in(values)) return query; } @Override public DaoMethodOutcome update(T theResource) { return update(theResource, null); } @Override public DaoMethodOutcome update(T theResource, String theMatchUrl) { return update(theResource, theMatchUrl, true); } @Override public DaoMethodOutcome update(T theResource, String theMatchUrl,boolean thePerformIndexing) { StopWatch w = new StopWatch(); preProcessResourceForStorage(theResource); final BaseResourceEntity entity; IIdType resourceId; if (isNotBlank(theMatchUrl)) { Set<Long> match = DaoUtils.processMatchUrl(theMatchUrl, myResourceType, myContext, getBaseFhirDao().getDao(myResourceType)); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessage(BaseFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size()); throw new PreconditionFailedException(msg); } else if (match.size() == 1) { Long pid = match.iterator().next(); entity = (BaseResourceEntity) myEntityManager.find(myResourceEntity, pid); resourceId = entity.getIdDt(); } else { return create(theResource, null, thePerformIndexing); } } else { resourceId = theResource.getId(); if (resourceId == null || isBlank(resourceId.getIdPart())) { throw new InvalidRequestException("Can not update a resource with no ID"); } try { entity = (BaseResourceEntity) myEntityManager.find(myResourceEntity, resourceId.getIdPartAsLong()); if (entity == null) { // Spec says that with ID, we either update or create if not exits. return create(theResource, null, thePerformIndexing); //throw new ResourceNotFoundException(resourceId); } // validateGivenIdIsAppropriateToRetrieveResource(resourceId, entity); } catch (ResourceNotFoundException e) { if (Character.isDigit(theResource.getId().getIdPart().charAt(0))) { throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart())); } return doCreate(theResource, null, thePerformIndexing); } } if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) { throw new InvalidRequestException("Trying to update " + resourceId + " but this is not the current version"); } BaseResourceEntity savedEntity = baseFhirDao.updateEntity(theResource, entity, true, null, thePerformIndexing, true); baseFhirDao.notifyWriteCompleted(); ourLog.info("Processed update on {} in {}ms", resourceId, w.getMillisAndRestart()); return toMethodOutcome(savedEntity, theResource).setCreated(false); } protected void preProcessResourceForStorage(T theResource) { if(getContext().getVersion().getVersion() == FhirVersionEnum.DSTU2 && createBundle){ Bundle bundle = (Bundle)theResource; if (bundle.getTypeElement().getValueAsEnum() != BundleTypeEnum.DOCUMENT) { String message = "Unable to store a Bundle resource on this server with a Bundle.type value other than '" + BundleTypeEnum.DOCUMENT.getCode() + "' - Value was: " + (bundle.getTypeElement().getValueAsEnum() != null ? bundle.getTypeElement().getValueAsEnum().getCode() : "(missing)"); throw new UnprocessableEntityException(message); } // bundle.setBase((UriDt)null); } } @Override public void notifyWriteCompleted() { getBaseFhirDao().notifyWriteCompleted(); } @Override public MethodOutcome validate(T theResource, IdDt theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile) { final OperationOutcome oo = new OperationOutcome(); IParser parser = theEncoding.newParser(getBaseFhirDao().getContext()); parser.setParserErrorHandler(new IParserErrorHandler() { @Override public void unknownAttribute(IParseLocation theLocation, String theAttributeName) { CodeableConceptDt detailTxt = new CodeableConceptDt(); detailTxt.setText("Unknown attribute found: " + theAttributeName); oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.INVALID_CONTENT).setDetails(detailTxt); } @Override public void unknownElement(IParseLocation theLocation, String theElementName) { CodeableConceptDt detailTxt = new CodeableConceptDt(); detailTxt.setText("Unknown element found: " + theElementName); oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.INVALID_CONTENT).setDetails(detailTxt); } @Override public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) { CodeableConceptDt detailTxt = new CodeableConceptDt(); detailTxt.setText("Multiple repetitions of non-repeatable element found: " + theElementName); oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.INVALID_CONTENT).setDetails(detailTxt); } //@Override //public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) { // oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setCode(IssueTypeEnum.INVALID_CONTENT).setDetails("Multiple repetitions of non-repeatable element found: " + theElementName); //} }); FhirValidator validator = getBaseFhirDao().getContext().newValidator(); validator.setValidateAgainstStandardSchema(true); validator.setValidateAgainstStandardSchematron(true); ValidationResult result = validator.validateWithResult(theResource); OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); for (BaseIssue next : operationOutcome.getIssue()) { oo.getIssue().add((Issue) next); } // This method returns a MethodOutcome object MethodOutcome retVal = new MethodOutcome(); oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Validation succeeded"); retVal.setOperationOutcome(oo); return retVal; } @Override public void registerDaoListener(IDaoListener theListener) { getBaseFhirDao().registerDaoListener(theListener); } }