Merge branch 'master' of github.com:jamesagnew/hapi-fhir

This commit is contained in:
jamesagnew 2017-12-13 07:05:41 -05:00
commit 96e3135793
17 changed files with 1585 additions and 285 deletions

View File

@ -22,6 +22,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
* #L%
*/
import java.util.*;
import java.util.function.Predicate;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
@ -212,10 +213,43 @@ public class FhirTerser {
@SuppressWarnings("unchecked")
private <T> List<T> getValues(BaseRuntimeElementCompositeDefinition<?> theCurrentDef, Object theCurrentObj, List<String> theSubList, Class<T> theWantedClass) {
String name = theSubList.get(0);
List<T> retVal = new ArrayList<>();
if (name.startsWith("extension('")) {
String extensionUrl = name.substring("extension('".length());
int endIndex = extensionUrl.indexOf('\'');
if (endIndex != -1) {
extensionUrl = extensionUrl.substring(0, endIndex);
}
List<ExtensionDt> extensions= Collections.emptyList();
if (theCurrentObj instanceof ISupportsUndeclaredExtensions) {
extensions = ((ISupportsUndeclaredExtensions) theCurrentObj).getUndeclaredExtensionsByUrl(extensionUrl);
} else if (theCurrentObj instanceof IBaseExtension) {
extensions = ((IBaseExtension)theCurrentObj).getExtension();
}
for (ExtensionDt next : extensions) {
if (theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
if (theSubList.size() > 1) {
List<T> values = retVal;
retVal = new ArrayList<>();
for (T nextElement : values) {
BaseRuntimeElementCompositeDefinition<?> nextChildDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition((Class<? extends IBase>) nextElement.getClass());
List<T> foundValues = getValues(nextChildDef, nextElement, theSubList.subList(1, theSubList.size()), theWantedClass);
retVal.addAll(foundValues);
}
}
return retVal;
}
BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(name);
List<? extends IBase> values = nextDef.getAccessor().getValues(theCurrentObj);
List<T> retVal = new ArrayList<T>();
if (theSubList.size() == 1) {
if (nextDef instanceof RuntimeChildChoiceDefinition) {
@ -268,7 +302,25 @@ public class FhirTerser {
}
private List<String> parsePath(BaseRuntimeElementCompositeDefinition<?> theElementDef, String thePath) {
List<String> parts = Arrays.asList(thePath.split("\\."));
List<String> parts = new ArrayList<>();
int currentStart = 0;
boolean inSingleQuote = false;
for (int i = 0; i < thePath.length(); i++) {
switch (thePath.charAt(i)) {
case '\'':
inSingleQuote = !inSingleQuote;
break;
case '.':
if (!inSingleQuote) {
parts.add(thePath.substring(currentStart, i));
currentStart = i + 1;
}
break;
}
}
parts.add(thePath.substring(currentStart));
if (theElementDef instanceof RuntimeResourceDefinition) {
if (parts.size() > 0 && parts.get(0).equals(theElementDef.getName())) {

View File

@ -56,8 +56,12 @@ 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.TransactionDefinition;
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 javax.annotation.PostConstruct;
import javax.persistence.NoResultException;
@ -89,6 +93,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private Class<T> myResourceType;
private String mySecondaryPrimaryKeyParamName;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -558,6 +564,26 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return false;
}
protected void markResourcesMatchingExpressionAsNeedingReindexing(String theExpression) {
if (isNotBlank(theExpression)) {
final String resourceType = theExpression.substring(0, theExpression.indexOf('.'));
ourLog.info("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, theExpression);
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
int updatedCount = txTemplate.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus theStatus) {
return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType);
}
});
ourLog.info("Marked {} resources for reindexing", updatedCount);
}
mySearchParamRegistry.forceRefresh();
}
@Override
public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) {
// Notify interceptors

View File

@ -27,6 +27,8 @@ import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ObjectUtils;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -80,7 +82,17 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
for (String nextPath : nextPathsSplit) {
String nextPathTrimmed = nextPath.trim();
try {
values.addAll(t.getValues(theResource, nextPathTrimmed));
List<Object> allValues = t.getValues(theResource, nextPathTrimmed);
for (Object next : allValues) {
if (next instanceof IBaseExtension) {
IBaseDatatype value = ((IBaseExtension) next).getValue();
if (value != null) {
values.add(value);
}
} else {
values.add(next);
}
}
} catch (Exception e) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
ourLog.warn("Failed to index values from path[{}] in resource type[{}]: {}", new Object[] { nextPathTrimmed, def.getName(), e.toString(), e } );

View File

@ -20,19 +20,34 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.CodeDt;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<SearchParameter>implements IFhirResourceDaoSearchParameter<SearchParameter> {
public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<SearchParameter> implements IFhirResourceDaoSearchParameter<SearchParameter> {
@Autowired
private IFhirSystemDao<Bundle, MetaDt> mySystemDao;
protected void markAffectedResources(SearchParameter theResource) {
markResourcesMatchingExpressionAsNeedingReindexing(theResource != null ? theResource.getXpath() : null);
}
/**
* This method is called once per minute to perform any required re-indexing. During most passes this will
* just check and find that there are no resources requiring re-indexing. In that case the method just returns
@ -40,7 +55,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<Se
* reindexing and then return.
*/
@Override
@Scheduled(fixedDelay=DateUtils.MILLIS_PER_MINUTE)
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE)
public void performReindexingPass() {
if (getConfig().isSchedulingDisabled()) {
return;
@ -58,4 +73,39 @@ public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<Se
}
@Override
protected void postPersist(ResourceTable theEntity, SearchParameter theResource) {
super.postPersist(theEntity, theResource);
markAffectedResources(theResource);
}
@Override
protected void postUpdate(ResourceTable theEntity, SearchParameter theResource) {
super.postUpdate(theEntity, theResource);
markAffectedResources(theResource);
}
@Override
protected void preDelete(SearchParameter theResourceToDelete, ResourceTable theEntityToDelete) {
super.preDelete(theResourceToDelete, theEntityToDelete);
markAffectedResources(theResourceToDelete);
}
@Override
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
super.validateResourceForStorage(theResource, theEntityToSave);
Enum<?> status = theResource.getStatusElement().getValueAsEnum();
List<BoundCodeDt<ResourceTypeEnum>> base = Collections.emptyList();
if (theResource.getBase() != null) {
base = Arrays.asList(theResource.getBaseElement());
}
String expression = theResource.getXpath();
FhirContext context = getContext();
SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
}
}

View File

@ -227,6 +227,15 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, new BigDecimal(nextValue.getValue()));
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
} else if (nextObject instanceof DecimalDt) {
DecimalDt nextValue = (DecimalDt) nextObject;
if (nextValue.getValue() == null) {
continue;
}
ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue());
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
} else {
if (!multiType) {
throw new ConfigurationException("Search param " + resourceName + " is of unexpected datatype: " + nextObject.getClass());

View File

@ -20,9 +20,234 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
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.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamRegistryDstu3.class);
public static final int MAX_MANAGED_PARAM_COUNT = 10000;
private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
@Autowired
private DaoConfig myDaoConfig;
private volatile long myLastRefresh;
@Autowired
private IFhirResourceDao<SearchParameter> mySpDao;
@Override
protected void refreshCacheIfNecessary() {
// nothing yet
public void forceRefresh() {
synchronized (this) {
myLastRefresh = 0;
}
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
refreshCacheIfNecessary();
return myActiveSearchParams;
}
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
refreshCacheIfNecessary();
return myActiveSearchParams.get(theResourceName);
}
private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
Map<String, RuntimeSearchParam> retVal = searchParams.get(theResourceName);
if (retVal == null) {
retVal = new HashMap<>();
searchParams.put(theResourceName, retVal);
}
return retVal;
}
protected void refreshCacheIfNecessary() {
long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE;
if (System.currentTimeMillis() - refreshInterval > myLastRefresh) {
synchronized (this) {
if (System.currentTimeMillis() - refreshInterval > myLastRefresh) {
StopWatch sw = new StopWatch();
Map<String, Map<String, RuntimeSearchParam>> searchParams = new HashMap<>();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : getBuiltInSearchParams().entrySet()) {
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
String nextResourceName = nextBuiltInEntry.getKey();
getSearchParamMap(searchParams, nextResourceName).put(nextParam.getName(), nextParam);
}
}
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
IBundleProvider allSearchParamsBp = mySpDao.search(params);
int size = allSearchParamsBp.size();
// Just in case..
if (size > MAX_MANAGED_PARAM_COUNT) {
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
size = MAX_MANAGED_PARAM_COUNT;
}
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
for (IBaseResource nextResource : allSearchParams) {
SearchParameter nextSp = (SearchParameter) nextResource;
JpaRuntimeSearchParam runtimeSp = toRuntimeSp(nextSp);
if (runtimeSp == null) {
continue;
}
CodeDt nextBaseName = nextSp.getBaseElement();
String resourceType = nextBaseName.getValue();
if (isBlank(resourceType)) {
continue;
}
Map<String, RuntimeSearchParam> searchParamMap = getSearchParamMap(searchParams, resourceType);
String name = runtimeSp.getName();
if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) {
searchParamMap.put(name, runtimeSp);
}
}
Map<String, Map<String, RuntimeSearchParam>> activeSearchParams = new HashMap<>();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextEntry : searchParams.entrySet()) {
for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) {
String nextName = nextSp.getName();
if (nextSp.getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE) {
nextSp = null;
}
if (!activeSearchParams.containsKey(nextEntry.getKey())) {
activeSearchParams.put(nextEntry.getKey(), new HashMap<String, RuntimeSearchParam>());
}
if (activeSearchParams.containsKey(nextEntry.getKey())) {
ourLog.debug("Replacing existing/built in search param {}:{} with new one", nextEntry.getKey(), nextName);
}
if (nextSp != null) {
activeSearchParams.get(nextEntry.getKey()).put(nextName, nextSp);
} else {
activeSearchParams.get(nextEntry.getKey()).remove(nextName);
}
}
}
myActiveSearchParams = activeSearchParams;
super.populateActiveSearchParams(activeSearchParams);
myLastRefresh = System.currentTimeMillis();
ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis());
}
}
}
}
private JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) {
String name = theNextSp.getCode();
String description = theNextSp.getDescription();
String path = theNextSp.getXpath();
RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
switch (theNextSp.getTypeElement().getValueAsEnum()) {
case COMPOSITE:
paramType = RestSearchParameterTypeEnum.COMPOSITE;
break;
case DATE_DATETIME:
paramType = RestSearchParameterTypeEnum.DATE;
break;
case NUMBER:
paramType = RestSearchParameterTypeEnum.NUMBER;
break;
case QUANTITY:
paramType = RestSearchParameterTypeEnum.QUANTITY;
break;
case REFERENCE:
paramType = RestSearchParameterTypeEnum.REFERENCE;
break;
case STRING:
paramType = RestSearchParameterTypeEnum.STRING;
break;
case TOKEN:
paramType = RestSearchParameterTypeEnum.TOKEN;
break;
case URI:
paramType = RestSearchParameterTypeEnum.URI;
break;
}
if (theNextSp.getStatus() != null) {
switch (theNextSp.getStatusElement().getValueAsEnum()) {
case ACTIVE:
status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
break;
case DRAFT:
status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
break;
case RETIRED:
status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
break;
}
}
Set<String> providesMembershipInCompartments = Collections.emptySet();
Set<String> targets = toStrings(theNextSp.getTarget());
if (isBlank(name) || isBlank(path) || paramType == null) {
if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
return null;
}
}
IIdType id = theNextSp.getIdElement();
String uri = "";
boolean unique = false;
List<ExtensionDt> uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE);
if (uniqueExts.size() > 0) {
IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive();
if (uniqueExtsValuePrimitive != null) {
if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
unique = true;
}
}
}
List<JpaRuntimeSearchParam.Component> components = Collections.emptyList();
Collection<? extends IPrimitiveType<String>> base = Arrays.asList(theNextSp.getBaseElement());
JpaRuntimeSearchParam retVal = new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, base);
return retVal;
}
private Set<String> toStrings(List<? extends CodeDt> theTarget) {
HashSet<String> retVal = new HashSet<String>();
for (CodeDt next : theTarget) {
if (isNotBlank(next.getValue())) {
retVal.add(next.getValue());
}
}
return retVal;
}
}

View File

@ -1,27 +1,23 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Enumerations;
import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.dstu3.model.SearchParameter;
import org.hl7.fhir.dstu3.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
@ -54,26 +50,7 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
private IFhirSystemDao<Bundle, Meta> mySystemDao;
protected void markAffectedResources(SearchParameter theResource) {
if (theResource != null) {
String expression = theResource.getExpression();
if (isNotBlank(expression)) {
final String resourceType = expression.substring(0, expression.indexOf('.'));
ourLog.info("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, expression);
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
int updatedCount = txTemplate.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus theStatus) {
return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType);
}
});
ourLog.info("Marked {} resources for reindexing", updatedCount);
}
}
mySearchParamRegistry.forceRefresh();
markResourcesMatchingExpressionAsNeedingReindexing(theResource != null ? theResource.getExpression() : null);
}
/**
@ -123,57 +100,13 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
super.validateResourceForStorage(theResource, theEntityToSave);
if (theResource.getStatus() == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid: " + theResource.getStatusElement().getValueAsString());
}
if (ElementUtil.isEmpty(theResource.getBase())) {
throw new UnprocessableEntityException("SearchParameter.base is missing");
}
Enum<?> status = theResource.getStatus();
List<CodeType> base = theResource.getBase();
String expression = theResource.getExpression();
if (theResource.getType() == Enumerations.SearchParamType.COMPOSITE && isBlank(expression)) {
// this is ok
} else if (isBlank(expression)) {
throw new UnprocessableEntityException("SearchParameter.expression is missing");
} else {
expression = expression.trim();
theResource.setExpression(expression);
String[] expressionSplit = BaseSearchParamExtractor.SPLIT.split(expression);
String allResourceName = null;
for (String nextPath : expressionSplit) {
nextPath = nextPath.trim();
int dotIdx = nextPath.indexOf('.');
if (dotIdx == -1) {
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\". Must start with a resource name");
}
String resourceName = nextPath.substring(0, dotIdx);
try {
getContext().getResourceDefinition(resourceName);
} catch (DataFormatException e) {
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\": " + e.getMessage());
}
if (allResourceName == null) {
allResourceName = resourceName;
} else {
if (!allResourceName.equals(resourceName)) {
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\". All paths in a single SearchParameter must match the same resource type");
}
}
}
} // if have expression
FhirContext context = getContext();
Enumerations.SearchParamType type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
}
}

View File

@ -1,7 +1,22 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
@ -23,59 +38,16 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* #L%
*/
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.SearchParameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil;
public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchParameter> implements IFhirResourceDaoSearchParameter<SearchParameter> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSearchParameterR4.class);
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IFhirSystemDao<Bundle, Meta> mySystemDao;
protected void markAffectedResources(SearchParameter theResource) {
if (theResource != null) {
String expression = theResource.getExpression();
if (isNotBlank(expression)) {
final String resourceType = expression.substring(0, expression.indexOf('.'));
ourLog.info("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, expression);
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
int updatedCount = txTemplate.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus theStatus) {
return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType);
}
});
ourLog.info("Marked {} resources for reindexing", updatedCount);
}
}
mySearchParamRegistry.forceRefresh();
markResourcesMatchingExpressionAsNeedingReindexing(theResource != null ? theResource.getExpression() : null);
}
/**
@ -125,29 +97,37 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
super.validateResourceForStorage(theResource, theEntityToSave);
if (theResource.getStatus() == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid: " + theResource.getStatusElement().getValueAsString());
Enum<?> status = theResource.getStatus();
List<CodeType> base = theResource.getBase();
String expression = theResource.getExpression();
FhirContext context = getContext();
Enum<?> type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
}
if (ElementUtil.isEmpty(theResource.getBase())) {
public static void validateSearchParam(Enum<?> theType, Enum<?> theStatus, List<? extends IPrimitiveType> theBase, String theExpression, FhirContext theContext) {
if (theStatus == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid");
}
if (ElementUtil.isEmpty(theBase)) {
throw new UnprocessableEntityException("SearchParameter.base is missing");
}
String expression = theResource.getExpression();
if (theResource.getType() == Enumerations.SearchParamType.COMPOSITE && isBlank(expression)) {
if (theType != null && theType.name().equals(Enumerations.SearchParamType.COMPOSITE.name()) && isBlank(theExpression)) {
// this is ok
} else if (isBlank(expression)) {
} else if (isBlank(theExpression)) {
throw new UnprocessableEntityException("SearchParameter.expression is missing");
} else {
expression = expression.trim();
theResource.setExpression(expression);
theExpression = theExpression.trim();
String[] expressionSplit = BaseSearchParamExtractor.SPLIT.split(expression);
String[] expressionSplit = BaseSearchParamExtractor.SPLIT.split(theExpression);
String allResourceName = null;
for (String nextPath : expressionSplit) {
nextPath = nextPath.trim();
@ -159,7 +139,7 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
String resourceName = nextPath.substring(0, dotIdx);
try {
getContext().getResourceDefinition(resourceName);
theContext.getResourceDefinition(resourceName);
} catch (DataFormatException e) {
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\": " + e.getMessage());
}
@ -175,7 +155,6 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
}
} // if have expression
}
}

View File

@ -54,16 +54,13 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv
List<String> destinationAddresses = new ArrayList<>();
String[] destinationAddressStrings = StringUtils.split(endpointUrl, ",");
for (String next : destinationAddressStrings) {
next = trim(defaultString(next));
if (next.startsWith("mailto:")) {
next = next.substring("mailto:".length());
}
next = processEmailAddressUri(next);
if (isNotBlank(next)) {
destinationAddresses.add(next);
}
}
String from = defaultString(subscription.getEmailDetails().getFrom(), mySubscriptionEmailInterceptor.getDefaultFromAddress());
String from = processEmailAddressUri(defaultString(subscription.getEmailDetails().getFrom(), mySubscriptionEmailInterceptor.getDefaultFromAddress()));
String subjectTemplate = defaultString(subscription.getEmailDetails().getSubjectTemplate(), provideDefaultSubjectTemplate());
EmailDetails details = new EmailDetails();
@ -77,6 +74,14 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv
emailSender.send(details);
}
private String processEmailAddressUri(String next) {
next = trim(defaultString(next));
if (next.startsWith("mailto:")) {
next = next.substring("mailto:".length());
}
return next;
}
private String provideDefaultSubjectTemplate() {
return "HAPI FHIR Subscriptions";

View File

@ -45,12 +45,17 @@ import static org.mockito.Mockito.mock;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestDstu2Config.class})
public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Autowired
protected ISearchParamRegistry mySearchParamRegsitry;
@Autowired
protected ApplicationContext myAppCtx;
@Autowired
@Qualifier("myAppointmentDaoDstu2")
protected IFhirResourceDao<Appointment> myAppointmentDao;
@Autowired
@Qualifier("mySearchParameterDaoDstu2")
protected IFhirResourceDao<SearchParameter> mySearchParameterDao;
@Autowired
@Qualifier("myBundleDaoDstu2")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired

View File

@ -0,0 +1,907 @@
package ca.uhn.fhir.jpa.dao.dstu2;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu2.resource.Appointment;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.Practitioner;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.*;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SearchCustomSearchParamTest.class);
@Before
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("Patient.gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("SearchParameter.base is missing", e.getMessage());
}
}
@Test
public void testCreateInvalidParamInvalidResourceName() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("PatientFoo.gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Invalid SearchParameter.expression value \"PatientFoo.gender\": Unknown resource name \"PatientFoo\" (this name is not known in FHIR version \"DSTU2\")", e.getMessage());
}
}
@Test
public void testCreateInvalidParamMismatchedResourceName() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("Patient.gender or Observation.code");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Invalid SearchParameter.expression value \"Observation.code\". All paths in a single SearchParameter must match the same resource type", e.getMessage());
}
}
@Test
public void testCreateInvalidParamNoPath() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("SearchParameter.expression is missing", e.getMessage());
}
}
@Test
public void testCreateInvalidParamNoResourceName() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Invalid SearchParameter.expression value \"gender\". Must start with a resource name", e.getMessage());
}
}
@Test
public void testCreateInvalidParamParamNullStatus() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("Patient.gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus((ConformanceResourceStatusEnum) null);
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("SearchParameter.status is missing or invalid", e.getMessage());
}
}
@Test
public void testCustomReferenceParameter() throws Exception {
SearchParameter sp = new SearchParameter();
sp.setBase(ResourceTypeEnum.PATIENT);
sp.setCode("myDoctor");
sp.setType(SearchParamTypeEnum.REFERENCE);
sp.setXpath("Patient.extension('http://fmcna.com/myDoctor')");
sp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
sp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(sp);
Practitioner pract = new Practitioner();
pract.setId("A");
pract.getName().addFamily("PRACT");
myPractitionerDao.update(pract);
Patient pat = new Patient();
pat.addUndeclaredExtension(false, "http://fmcna.com/myDoctor").setValue(new ResourceReferenceDt("Practitioner/A"));
IIdType pid = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap params = new SearchParameterMap();
params.add("myDoctor", new ReferenceParam("A"));
IBundleProvider outcome = myPatientDao.search(params);
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
ourLog.info("IDS: " + ids);
assertThat(ids, contains(pid.getValue()));
}
@Test
public void testExtensionWithNoValueIndexesWithoutFailure() {
SearchParameter eyeColourSp = new SearchParameter();
eyeColourSp.setBase(ResourceTypeEnum.PATIENT);
eyeColourSp.setCode("eyecolour");
eyeColourSp.setType(SearchParamTypeEnum.TOKEN);
eyeColourSp.setXpath("Patient.extension('http://acme.org/eyecolour')");
eyeColourSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
eyeColourSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(eyeColourSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient p1 = new Patient();
p1.setActive(true);
p1.addUndeclaredExtension(false, "http://acme.org/eyecolour").addUndeclaredExtension(false, "http://foo").setValue(new StringDt("VAL"));
IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
}
@Test
public void testIncludeExtensionReferenceAsRecurse() {
SearchParameter attendingSp = new SearchParameter();
attendingSp.setBase(ResourceTypeEnum.PATIENT);
attendingSp.setCode("attending");
attendingSp.setType(SearchParamTypeEnum.REFERENCE);
attendingSp.setXpath("Patient.extension('http://acme.org/attending')");
attendingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
attendingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
attendingSp.addTarget(ResourceTypeEnum.PRACTITIONER);
IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Practitioner p1 = new Practitioner();
p1.getName().addFamily("P1");
IIdType p1id = myPractitionerDao.create(p1).getId().toUnqualifiedVersionless();
Patient p2 = new Patient();
p2.addName().addFamily("P2");
p2.addUndeclaredExtension(false, "http://acme.org/attending").setValue(new ResourceReferenceDt(p1id));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
Appointment app = new Appointment();
app.addParticipant().getActor().setReference(p2id.getValue());
IIdType appId = myAppointmentDao.create(app).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.addInclude(new Include("Appointment:patient", true));
map.addInclude(new Include("Patient:attending", true));
results = myAppointmentDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(appId.getValue(), p2id.getValue(), p1id.getValue()));
}
@Test
public void testSearchForExtensionReferenceWithNonMatchingTarget() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("sibling");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/sibling')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient p1 = new Patient();
p1.addName().addFamily("P1");
IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
Patient p2 = new Patient();
p2.addName().addFamily("P2");
p2.addUndeclaredExtension(false, "http://acme.org/sibling").setValue(new ResourceReferenceDt(p1id));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Search by ref
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam(p1id.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
// Search by chain
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam("name", "P1"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test
public void testSearchForExtensionReferenceWithTarget() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("sibling");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/sibling')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.PATIENT);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient p1 = new Patient();
p1.addName().addFamily("P1");
IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
Patient p2 = new Patient();
p2.addName().addFamily("P2");
p2.addUndeclaredExtension(false, "http://acme.org/sibling").setValue(new ResourceReferenceDt(p1id));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
Appointment app = new Appointment();
app.addParticipant().getActor().setReference(p2id.getValue());
IIdType appid = myAppointmentDao.create(app).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Search by ref
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam(p1id.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
// Search by chain
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam("name", "P1"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
// Search by two level chain
map = new SearchParameterMap();
map.add("patient", new ReferenceParam("sibling.name", "P1"));
results = myAppointmentDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, containsInAnyOrder(appid.getValue()));
}
@Test
public void testSearchForExtensionReferenceWithoutTarget() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("sibling");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/sibling')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient p1 = new Patient();
p1.addName().addFamily("P1");
IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
Patient p2 = new Patient();
p2.addName().addFamily("P2");
p2.addUndeclaredExtension(false, "http://acme.org/sibling").setValue(new ResourceReferenceDt(p1id));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
Appointment app = new Appointment();
app.addParticipant().getActor().setReference(p2id.getValue());
IIdType appid = myAppointmentDao.create(app).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Search by ref
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam(p1id.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
// Search by chain
map = new SearchParameterMap();
map.add("sibling", new ReferenceParam("name", "P1"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
// Search by two level chain
map = new SearchParameterMap();
map.add("patient", new ReferenceParam("sibling.name", "P1"));
results = myAppointmentDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, containsInAnyOrder(appid.getValue()));
}
@Test
public void testSearchForExtensionToken() {
SearchParameter eyeColourSp = new SearchParameter();
eyeColourSp.setBase(ResourceTypeEnum.PATIENT);
eyeColourSp.setCode("eyecolour");
eyeColourSp.setType(SearchParamTypeEnum.TOKEN);
eyeColourSp.setXpath("Patient.extension('http://acme.org/eyecolour')");
eyeColourSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
eyeColourSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(eyeColourSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient p1 = new Patient();
p1.setActive(true);
p1.addUndeclaredExtension(false, "http://acme.org/eyecolour").setValue(new CodeDt("blue"));
IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
Patient p2 = new Patient();
p2.setActive(true);
p2.addUndeclaredExtension(false, "http://acme.org/eyecolour").setValue(new CodeDt("green"));
IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless();
// Try with custom gender SP
SearchParameterMap map = new SearchParameterMap();
map.add("eyecolour", new TokenParam(null, "blue"));
IBundleProvider results = myPatientDao.search(map);
List<String> foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p1id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepCodeableConcept() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.TOKEN);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false, "http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new CodeableConceptDt().addCoding(new CodingDt().setSystem("foo").setCode("bar")));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new TokenParam("foo", "bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepCoding() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.TOKEN);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new CodingDt().setSystem("foo").setCode("bar"));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new TokenParam("foo", "bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepDate() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.DATE_DATETIME);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Appointment apt = new Appointment();
apt.setStatus(AppointmentStatusEnum.ARRIVED);
IIdType aptId = myAppointmentDao.create(apt).getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new DateDt("2012-01-02"));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new DateParam("2012-01-02"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepDecimal() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.NUMBER);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new DecimalDt("2.1"));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new NumberParam("2.1"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepNumber() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.NUMBER);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new IntegerDt(5));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new NumberParam("5"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepReference() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.APPOINTMENT);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Appointment apt = new Appointment();
apt.setStatus(AppointmentStatusEnum.ARRIVED);
IIdType aptId = myAppointmentDao.create(apt).getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false, "http://acme.org/foo");
extParent
.addUndeclaredExtension(false, "http://acme.org/bar")
.setValue(new ResourceReferenceDt(aptId.getValue()));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new ReferenceParam(aptId.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepReferenceWithoutType() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Appointment apt = new Appointment();
apt.setStatus(AppointmentStatusEnum.ARRIVED);
IIdType aptId = myAppointmentDao.create(apt).getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new ResourceReferenceDt(aptId.getValue()));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new ReferenceParam(aptId.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForExtensionTwoDeepReferenceWrongType() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.REFERENCE);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
siblingSp.addTarget(ResourceTypeEnum.OBSERVATION);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Appointment apt = new Appointment();
apt.setStatus(AppointmentStatusEnum.ARRIVED);
IIdType aptId = myAppointmentDao.create(apt).getId().toUnqualifiedVersionless();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new ResourceReferenceDt(aptId.getValue()));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new ReferenceParam(aptId.getValue()));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test
public void testSearchForExtensionTwoDeepString() {
SearchParameter siblingSp = new SearchParameter();
siblingSp.setBase(ResourceTypeEnum.PATIENT);
siblingSp.setCode("foobar");
siblingSp.setType(SearchParamTypeEnum.STRING);
siblingSp.setXpath("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')");
siblingSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(siblingSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient patient = new Patient();
patient.addName().addFamily("P2");
ExtensionDt extParent = patient
.addUndeclaredExtension(false,
"http://acme.org/foo");
extParent
.addUndeclaredExtension(false,
"http://acme.org/bar")
.setValue(new StringDt("HELLOHELLO"));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
map = new SearchParameterMap();
map.add("foobar", new StringParam("hello"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(p2id.getValue()));
}
@Test
public void testSearchForStringOnIdentifier() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.STRING);
fooSp.setXpath("Patient.identifier.value");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.addIdentifier().setSystem("FOO123").setValue("BAR678");
pat.setGender(AdministrativeGenderEnum.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat.setGender(AdministrativeGenderEnum.FEMALE);
myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Partial match
map = new SearchParameterMap();
map.add("foo", new StringParam("bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Non match
map = new SearchParameterMap();
map.add("foo", new StringParam("zzz"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test
public void testSearchWithCustomParam() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("Patient.gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.setGender(AdministrativeGenderEnum.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat.setGender(AdministrativeGenderEnum.FEMALE);
IIdType patId2 = myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Try with custom gender SP
map = new SearchParameterMap();
map.add("foo", new TokenParam(null, "male"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Try with normal gender SP
map = new SearchParameterMap();
map.add("gender", new TokenParam(null, "male"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Delete the param
mySearchParameterDao.delete(spId, mySrd);
mySearchParamRegsitry.forceRefresh();
mySystemDao.performReindexingPass(100);
// Try with custom gender SP
map = new SearchParameterMap();
map.add("foo", new TokenParam(null, "male"));
try {
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
}
}
@Test
public void testSearchWithCustomParamDraft() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.TOKEN);
fooSp.setXpath("Patient.gender");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.DRAFT);
mySearchParameterDao.create(fooSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.setGender(AdministrativeGenderEnum.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat.setGender(AdministrativeGenderEnum.FEMALE);
IIdType patId2 = myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Try with custom gender SP (should find nothing)
map = new SearchParameterMap();
map.add("foo", new TokenParam(null, "male"));
try {
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
}
// Try with normal gender SP
map = new SearchParameterMap();
map.add("gender", new TokenParam(null, "male"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -130,7 +130,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("SearchParameter.status is missing or invalid: null", e.getMessage());
assertEquals("SearchParameter.status is missing or invalid", e.getMessage());
}
}

View File

@ -132,7 +132,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("SearchParameter.status is missing or invalid: null", e.getMessage());
assertEquals("SearchParameter.status is missing or invalid", e.getMessage());
}
}
@ -812,6 +812,50 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
}
@Test
public void testSearchForStringOnIdentifier() {
SearchParameter fooSp = new SearchParameter();
fooSp.addBase("Patient");
fooSp.setCode("foo");
fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.STRING);
fooSp.setTitle("FOO SP");
fooSp.setExpression("Patient.identifier.value");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.addIdentifier().setSystem("FOO123").setValue("BAR678");
pat.setGender(AdministrativeGender.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat.setGender(AdministrativeGender.FEMALE);
myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Partial match
map = new SearchParameterMap();
map.add("foo", new StringParam("bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Non match
map = new SearchParameterMap();
map.add("foo", new StringParam("zzz"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test
public void testSearchWithCustomParamDraft() {

View File

@ -176,6 +176,53 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription sub1 = createSubscription(criteria1, payload);
Subscription subscriptionTemp = ourClient.read(Subscription.class, sub1.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("mailto:myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(subscriptionTemp));
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
waitForQueueToDrain();
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
waitForSize(1, 20000, new Callable<Number>() {
@Override
public Number call() throws Exception {
return ourTestSmtp.getReceivedMessages().length;
}
});
List<MimeMessage> received = Arrays.asList(ourTestSmtp.getReceivedMessages());
assertEquals(1, received.size());
assertEquals(1, received.get(0).getFrom().length);
assertEquals("myfrom@from.com", ((InternetAddress)received.get(0).getFrom()[0]).getAddress());
assertEquals(1, received.get(0).getAllRecipients().length);
assertEquals("foo@example.com", ((InternetAddress)received.get(0).getAllRecipients()[0]).getAddress());
assertEquals("text/plain; charset=us-ascii", received.get(0).getContentType());
assertEquals("This is a subject", received.get(0).getSubject().toString().trim());
assertEquals("This is the body", received.get(0).getContent().toString().trim());
assertEquals(mySubscriptionIds.get(0).toUnqualifiedVersionless().getValue(), received.get(0).getHeader("X-FHIR-Subscription")[0]);
}
@Test
public void testEmailSubscriptionWithCustomNoMailtoOnFrom() throws Exception {
String payload = "This is the body";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription sub1 = createSubscription(criteria1, payload);
Subscription subscriptionTemp = ourClient.read(Subscription.class, sub1.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()

View File

@ -92,9 +92,6 @@ public interface IServerInterceptor {
/**
* This method is called just before the actual implementing server method is invoked.
* <p>
* Note about exceptions:
* </p>
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
@ -157,77 +154,40 @@ public interface IServerInterceptor {
boolean incomingRequestPreProcessed(HttpServletRequest theRequest, HttpServletResponse theResponse);
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR.
*/
@Deprecated
boolean outgoingResponse(RequestDetails theRequestDetails);
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theServletRequest
* The incoming request
* @param theServletResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR.
*/
@Deprecated
boolean outgoingResponse(RequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException;
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseObject
* The actual object which is being streamed to the client as a response
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR.
*/
@Deprecated
boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject);
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* response back to the client.
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseObject
* The actual object which is being streamed to the client as a response
* The actual object which is being streamed to the client as a response. This may be
* <code>null</code> if the response does not include a resource.
* @param theServletRequest
* The incoming request
* @param theServletResponse
@ -246,49 +206,19 @@ public interface IServerInterceptor {
throws AuthenticationException;
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseObject
* The actual object which is being streamed to the client as a response
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR.
*/
@Deprecated
boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject);
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
*
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseObject
* The actual object which is being streamed to the client as a response
* @param theServletRequest
* The incoming request
* @param theServletResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR.
*/
@Deprecated
boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException;
/**
@ -328,7 +258,7 @@ public interface IServerInterceptor {
*/
void processingCompletedNormally(ServletRequestDetails theRequestDetails);
public static class ActionRequestDetails {
class ActionRequestDetails {
private final FhirContext myContext;
private final IIdType myId;
private final String myResourceType;

View File

@ -1,13 +1,12 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@ -15,6 +14,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.rest.annotation.Create;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
@ -26,7 +26,9 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
@ -56,7 +58,7 @@ public class InterceptorDstu3Test {
@After
public void after() {
for (IServerInterceptor next : new ArrayList<IServerInterceptor>(ourServlet.getInterceptors())) {
for (IServerInterceptor next : new ArrayList<>(ourServlet.getInterceptors())) {
ourServlet.unregisterInterceptor(next);
}
}
@ -65,7 +67,6 @@ public class InterceptorDstu3Test {
public void before() {
myInterceptor1 = mock(IServerInterceptor.class);
myInterceptor2 = mock(IServerInterceptor.class);
ourServlet.setInterceptors(myInterceptor1, myInterceptor2);
}
@SuppressWarnings("deprecation")
@ -79,7 +80,72 @@ public class InterceptorDstu3Test {
}
@Test
public void testValidate() throws Exception {
public void testResponseWithOperationOutcome() throws Exception {
ourServlet.setInterceptors(myInterceptor1);
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
String input = createInput();
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$validate");
httpPost.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
IOUtils.closeQuietly(status.getEntity().getContent());
InOrder order = inOrder(myInterceptor1);
order.verify(myInterceptor1, times(1)).incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor1, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
ArgumentCaptor<RestOperationTypeEnum> opTypeCapt = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
ArgumentCaptor<ActionRequestDetails> arTypeCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<IBaseResource> resourceCapt = ArgumentCaptor.forClass(IBaseResource.class);
order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), resourceCapt.capture());
assertEquals(1, resourceCapt.getAllValues().size());
assertEquals(OperationOutcome.class, resourceCapt.getAllValues().get(0).getClass());
}
@Test
public void testResponseWithNothing() throws Exception {
ourServlet.setInterceptors(myInterceptor1);
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
String input = createInput();
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
try {
assertEquals(201, status.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
InOrder order = inOrder(myInterceptor1);
verify(myInterceptor1, times(1)).incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(myInterceptor1, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
ArgumentCaptor<RestOperationTypeEnum> opTypeCapt = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
ArgumentCaptor<ActionRequestDetails> arTypeCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<RequestDetails> rdCapt = ArgumentCaptor.forClass(RequestDetails.class);
ArgumentCaptor<IBaseResource> resourceCapt = ArgumentCaptor.forClass(IBaseResource.class);
verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), resourceCapt.capture());
assertEquals(1, resourceCapt.getAllValues().size());
assertEquals(null, resourceCapt.getAllValues().get(0));
// assertEquals("", rdCapt.getAllValues().get(0).get)
}
@Test
public void testResourceResponseIncluded() throws Exception {
ourServlet.setInterceptors(myInterceptor1, myInterceptor2);
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
@ -87,27 +153,7 @@ public class InterceptorDstu3Test {
when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
//@formatter:off
String input =
"{\n" +
" \"resourceType\":\"Observation\",\n" +
" \"id\":\"1855669\",\n" +
" \"meta\":{\n" +
" \"versionId\":\"1\",\n" +
" \"lastUpdated\":\"2016-02-18T07:41:35.953-05:00\"\n" +
" },\n" +
" \"status\":\"final\",\n" +
" \"subject\":{\n" +
" \"reference\":\"Patient/1\"\n" +
" },\n" +
" \"effectiveDateTime\":\"2016-02-18T07:45:36-05:00\",\n" +
" \"valueQuantity\":{\n" +
" \"value\":57,\n" +
" \"system\":\"http://unitsofmeasure.org\",\n" +
" \"code\":\"{Beats}/min\"\n" +
" }\n" +
"}";
//@formatter:on
String input = createInput();
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$validate");
httpPost.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));
@ -123,8 +169,8 @@ public class InterceptorDstu3Test {
ArgumentCaptor<ActionRequestDetails> arTypeCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class));
// Avoid concurrency issues
Thread.sleep(500);
@ -138,6 +184,18 @@ public class InterceptorDstu3Test {
assertNotNull(arTypeCapt.getValue().getResource());
}
private String createInput() {
return "{\n" +
" \"resourceType\":\"Patient\",\n" +
" \"id\":\"1855669\",\n" +
" \"meta\":{\n" +
" \"versionId\":\"1\",\n" +
" \"lastUpdated\":\"2016-02-18T07:41:35.953-05:00\"\n" +
" },\n" +
" \"active\":true\n" +
"}";
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
@ -177,6 +235,12 @@ public class InterceptorDstu3Test {
public MethodOutcome validate(@ResourceParam Patient theResource) {
return new MethodOutcome();
}
@Create()
public MethodOutcome create(@ResourceParam Patient theResource) {
return new MethodOutcome();
}
}
}

View File

@ -7,6 +7,11 @@
</properties>
<body>
<release version="3.2.0" date="TBD">
<action type="add">
Support for custom search parameters has been backported in the JPA server
from DSTU3 back to DSTU2. As of this release of HAPI, full support for custom
search parameters exists in all supported versions of FHIR.
</action>
<action type="fix">
Fix a crash in JPA server when performing a recursive
<![CDATA[<code>_include</code>]]> which doesn't actually find any matches.
@ -33,6 +38,13 @@
A few log entries emitted by the JPA server suring every search have been reduced
from INFO to DEBUG in order to reduce log noise
</action>
<action type="remove">
A few redundant and no longer useful methods have been marked as
deprecated in
<![CDATA[<code>IServerInterceptor</code>]]>. If you have implemented
custom interceptors you are recommended to migrate to the recommended
methods.
</action>
</release>
<release version="3.1.0" date="2017-11-23">
<action type="add">