Add support for $validate-code in JPA server

This commit is contained in:
James Agnew 2015-08-20 14:27:19 -04:00
parent 257908cf0a
commit ddc66d3ed0
17 changed files with 640 additions and 126 deletions

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.model.primitive; package ca.uhn.fhir.model.primitive;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import net.sourceforge.cobertura.CoverageIgnore; import net.sourceforge.cobertura.CoverageIgnore;

View File

@ -59,16 +59,16 @@ public @interface OperationParam {
Class<? extends IBase> type() default IBase.class; Class<? extends IBase> type() default IBase.class;
/** /**
* The minimum number of repetitions allowed for this child * The minimum number of repetitions allowed for this child (default is 0)
*/ */
int min() default 0; int min() default 0;
/** /**
* The maximum number of repetitions allowed for this child. Should be * The maximum number of repetitions allowed for this child. Should be
* set to {@link #MAX_UNLIMITED} if there is no limit to the number of * set to {@link #MAX_UNLIMITED} if there is no limit to the number of
* repetitions. * repetitions (default is 1)
*/ */
int max() default MAX_UNLIMITED; int max() default 1;
} }

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.rest.client.interceptor; package ca.uhn.fhir.rest.client.interceptor;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpRequestBase;

View File

@ -174,6 +174,10 @@
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId> <artifactId>spring-context-support</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<!-- Hibernate --> <!-- Hibernate -->
<dependency> <dependency>

View File

@ -19,8 +19,8 @@ package ca.uhn.fhir.jpa.dao;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
@ -115,7 +115,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
public abstract class BaseHapiFhirDao implements IDao { public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
@ -344,7 +344,7 @@ public abstract class BaseHapiFhirDao implements IDao {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected <T extends IBaseResource> IFhirResourceDao<T> getDao(Class<T> theType) { protected <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
if (myResourceTypeToDao == null) { if (myResourceTypeToDao == null) {
myResourceTypeToDao = new HashMap<Class<? extends IBaseResource>, IFhirResourceDao<?>>(); myResourceTypeToDao = new HashMap<Class<? extends IBaseResource>, IFhirResourceDao<?>>();
for (IFhirResourceDao<?> next : myResourceDaos) { for (IFhirResourceDao<?> next : myResourceDaos) {
@ -358,7 +358,7 @@ public abstract class BaseHapiFhirDao implements IDao {
} }
return (IFhirResourceDao<T>) myResourceTypeToDao.get(theType); return (IFhirResourceDao<R>) myResourceTypeToDao.get(theType);
} }
protected TagDefinition getTag(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { protected TagDefinition getTag(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -644,12 +644,12 @@ public abstract class BaseHapiFhirDao implements IDao {
} }
protected <T extends IResource> Set<Long> processMatchUrl(String theMatchUrl, Class<T> theResourceType) { protected <R extends IResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType); RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType);
SearchParameterMap paramMap = translateMatchUrl(theMatchUrl, resourceDef); SearchParameterMap paramMap = translateMatchUrl(theMatchUrl, resourceDef);
IFhirResourceDao<T> dao = getDao(theResourceType); IFhirResourceDao<R> dao = getDao(theResourceType);
Set<Long> ids = dao.searchForIdsWithAndOr(paramMap); Set<Long> ids = dao.searchForIdsWithAndOr(paramMap);
return ids; return ids;
@ -899,6 +899,7 @@ public abstract class BaseHapiFhirDao implements IDao {
case DSTU1: case DSTU1:
mySearchParamExtractor = new SearchParamExtractorDstu1(theContext); mySearchParamExtractor = new SearchParamExtractorDstu1(theContext);
break; break;
case DSTU2_HL7ORG:
case DEV: case DEV:
throw new IllegalStateException("Don't know how to handle version: " + myContext.getVersion().getVersion()); throw new IllegalStateException("Don't know how to handle version: " + myContext.getVersion().getVersion());
} }
@ -917,7 +918,7 @@ public abstract class BaseHapiFhirDao implements IDao {
return toResource(type.getImplementingClass(), theEntity); return toResource(type.getImplementingClass(), theEntity);
} }
protected <T extends IBaseResource> T toResource(Class<T> theResourceType, BaseHasResource theEntity) { protected <R extends IBaseResource> R toResource(Class<R> theResourceType, BaseHasResource theEntity) {
String resourceText = null; String resourceText = null;
switch (theEntity.getEncoding()) { switch (theEntity.getEncoding()) {
case JSON: case JSON:
@ -933,7 +934,7 @@ public abstract class BaseHapiFhirDao implements IDao {
} }
IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion()));
T retVal; R retVal;
try { try {
retVal = parser.parseResource(theResourceType, resourceText); retVal = parser.parseResource(theResourceType, resourceText);
} catch (Exception e) { } catch (Exception e) {
@ -955,9 +956,9 @@ public abstract class BaseHapiFhirDao implements IDao {
IResource res = (IResource) retVal; IResource res = (IResource) retVal;
res.setId(theEntity.getIdDt()); res.setId(theEntity.getIdDt());
res.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, theEntity.getVersion()); ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
res.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, theEntity.getPublished()); ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
res.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, theEntity.getUpdated()); ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
if (theEntity.getTitle() != null) { if (theEntity.getTitle() != null) {
ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle()); ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle());
@ -1038,9 +1039,10 @@ public abstract class BaseHapiFhirDao implements IDao {
return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true); return updateEntity(theResource, entity, theUpdateHistory, theDeletedTimestampOrNull, true, true);
} }
@SuppressWarnings("unchecked")
protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion) { protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion) {
validateResourceForStorage(theResource); validateResourceForStorage((T) theResource);
if (entity.getPublished() == null) { if (entity.getPublished() == null) {
entity.setPublished(new Date()); entity.setPublished(new Date());
@ -1216,12 +1218,15 @@ public abstract class BaseHapiFhirDao implements IDao {
} }
/** /**
* Subclasses may override to provide specific behaviour * This method is invoked immediately before storing a new resource, or an
* update to an existing resource to allow the DAO to ensure that it is valid
* for persistence. By default, no validation is performed, but subclasses
* may override to provide specific behaviour.
* *
* @param theResource * @param theResource
* The resource that is about to be persisted * The resource that is about to be persisted
*/ */
protected void validateResourceForStorage(IResource theResource) { protected void validateResourceForStorage(T theResource) {
// nothing // nothing
} }

View File

@ -131,7 +131,7 @@ import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ObjectUtil; import ca.uhn.fhir.util.ObjectUtil;
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseHapiFhirDao implements IFhirResourceDao<T> { public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
static final String OO_SEVERITY_WARN = "warning"; static final String OO_SEVERITY_WARN = "warning";
static final String OO_SEVERITY_INFO = "information"; static final String OO_SEVERITY_INFO = "information";

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
@ -7,7 +27,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse;
@ -39,7 +58,7 @@ public class FhirResourceDaoQuestionnaireResponseDstu2 extends FhirResourceDaoDs
} }
@Override @Override
protected void validateResourceForStorage(IResource theResource) { protected void validateResourceForStorage(QuestionnaireResponse theResource) {
super.validateResourceForStorage(theResource); super.validateResourceForStorage(theResource);
if (!myValidateResponses) { if (!myValidateResponses) {
return; return;

View File

@ -0,0 +1,187 @@
package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.commons.codec.binary.StringUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeInclude;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeIncludeConcept;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2<ValueSet>implements IFhirResourceDaoValueSet<ValueSet> {
@Override
public ValueSet expand(IIdType theId, StringDt theFilter) {
ValueSet retVal = new ValueSet();
retVal.setDate(DateTimeDt.withCurrentTime());
BaseHasResource sourceEntity = readEntity(theId);
if (sourceEntity == null) {
throw new ResourceNotFoundException(theId);
}
ValueSet source = (ValueSet) toResource(sourceEntity);
/*
* Add composed concepts
*/
for (ComposeInclude nextInclude : source.getCompose().getInclude()) {
for (ComposeIncludeConcept next : nextInclude.getConcept()) {
if (theFilter == null || theFilter.isEmpty()) {
addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
} else {
String filter = theFilter.getValue().toLowerCase();
if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
}
}
}
}
/*
* Add defined concepts
*/
for (CodeSystemConcept next : source.getCodeSystem().getConcept()) {
addCompose(theFilter, retVal, source, next);
}
return retVal;
}
private void addCompose(StringDt theFilter, ValueSet theValueSetToPopulate, ValueSet theSourceValueSet, CodeSystemConcept theConcept) {
if (theFilter == null || theFilter.isEmpty()) {
addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
} else {
String filter = theFilter.getValue().toLowerCase();
if (theConcept.getDisplay().toLowerCase().contains(filter) || theConcept.getCode().toLowerCase().contains(filter)) {
addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
}
}
for (CodeSystemConcept nextChild : theConcept.getConcept()) {
addCompose(theFilter, theValueSetToPopulate, theSourceValueSet, nextChild);
}
}
private void addCompose(ValueSet retVal, String theSystem, String theCode, String theDisplay) {
if (isBlank(theCode)) {
return;
}
ExpansionContains contains = retVal.getExpansion().addContains();
contains.setSystem(theSystem);
contains.setCode(theCode);
contains.setDisplay(theDisplay);
}
@Override
public ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult validateCode(UriDt theValueSetIdentifier, IIdType theId, CodeDt theCode, UriDt theSystem, StringDt theDisplay,
CodingDt theCoding, CodeableConceptDt theCodeableConcept) {
List<IIdType> valueSetIds;
boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0;
boolean haveCoding = theCoding != null && theCoding.isEmpty() == false;
boolean haveCode = theCode != null && theCode.isEmpty() == false;
if (!haveCodeableConcept && !haveCoding && !haveCode) {
throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate");
}
if (!(haveCodeableConcept ^ haveCoding ^ haveCode)) {
throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)");
}
boolean haveIdentifierParam = theValueSetIdentifier != null && theValueSetIdentifier.isEmpty() == false;
if (theId != null) {
valueSetIds = Collections.singletonList((IIdType) theId);
} else if (haveIdentifierParam) {
Set<Long> ids = searchForIds(ValueSet.SP_IDENTIFIER, new TokenParam(null, theValueSetIdentifier.getValue()));
valueSetIds = new ArrayList<IIdType>();
for (Long next : ids) {
valueSetIds.add(new IdDt("ValueSet", next));
}
} else {
if (theCode == null || theCode.isEmpty()) {
throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate.");
}
Set<Long> ids = searchForIds(ValueSet.SP_CODE, new TokenParam(toStringOrNull(theSystem), theCode.getValue()));
valueSetIds = new ArrayList<IIdType>();
for (Long next : ids) {
valueSetIds.add(new IdDt("ValueSet", next));
}
}
for (IIdType nextId : valueSetIds) {
ValueSet expansion = expand(nextId, null);
List<ExpansionContains> contains = expansion.getExpansion().getContains();
ValidateCodeResult result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept);
if (result != null) {
if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) {
if (!theDisplay.getValue().equals(result.getDisplay())) {
return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay());
}
}
return result;
}
}
return new ValidateCodeResult(false, "Code not found", null);
}
private String toStringOrNull(IPrimitiveType<String> thePrimitive) {
return thePrimitive != null ? thePrimitive.getValue() : null;
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInContains(List<ExpansionContains> contains, String theSystem, String theCode, CodingDt theCoding,
CodeableConceptDt theCodeableConcept) {
for (ExpansionContains nextCode : contains) {
ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept);
if (result != null) {
return result;
}
String system = nextCode.getSystem();
String code = nextCode.getCode();
if (isNotBlank(theCode)) {
if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) {
return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
}
} else if (theCoding != null) {
if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) {
return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
}
} else {
for (CodingDt next : theCodeableConcept.getCoding()) {
if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) {
return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,44 @@
package ca.uhn.fhir.jpa.dao;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
public interface IFhirResourceDaoValueSet<T extends IBaseResource> extends IFhirResourceDao<T> {
ValueSet expand(IIdType theId, StringDt theFilter);
ValidateCodeResult validateCode(UriDt theValueSetIdentifier, IIdType theId, CodeDt theCode, UriDt theSystem, StringDt theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept);
public class ValidateCodeResult {
private String myDisplay;
private String myMessage;
private boolean myResult;
public ValidateCodeResult(boolean theResult, String theMessage, String theDisplay) {
super();
myResult = theResult;
myMessage = theMessage;
myDisplay = theDisplay;
}
public String getDisplay() {
return myDisplay;
}
public String getMessage() {
return myMessage;
}
public boolean isResult() {
return myResult;
}
}
}

View File

@ -63,6 +63,7 @@ import ca.uhn.fhir.model.dstu2.resource.Conformance.RestSecurity;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.Patient.Communication; import ca.uhn.fhir.model.dstu2.resource.Patient.Communication;
import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.dstu2.valueset.RestfulSecurityServiceEnum; import ca.uhn.fhir.model.dstu2.valueset.RestfulSecurityServiceEnum;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt; import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.IntegerDt;
@ -405,6 +406,12 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
public List<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IResource theResource) { public List<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IResource theResource) {
ArrayList<BaseResourceIndexedSearchParam> retVal = new ArrayList<BaseResourceIndexedSearchParam>(); ArrayList<BaseResourceIndexedSearchParam> retVal = new ArrayList<BaseResourceIndexedSearchParam>();
String useSystem = null;
if (theResource instanceof ValueSet) {
ValueSet vs = (ValueSet) theResource;
useSystem = vs.getCodeSystem().getSystem();
}
RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource); RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource);
for (RuntimeSearchParam nextSpDef : def.getSearchParams()) { for (RuntimeSearchParam nextSpDef : def.getSearchParams()) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.TOKEN) { if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.TOKEN) {
@ -466,7 +473,11 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
if (nextValue.isEmpty()) { if (nextValue.isEmpty()) {
continue; continue;
} }
systems.add(null); if ("ValueSet.codeSystem.concept.code".equals(nextPath)) {
systems.add(useSystem);
} else {
systems.add(null);
}
codes.add(nextValue.getValueAsString()); codes.add(nextValue.getValueAsString());
} else if (nextObject instanceof CodingDt) { } else if (nextObject instanceof CodingDt) {
CodingDt nextValue = (CodingDt) nextObject; CodingDt nextValue = (CodingDt) nextObject;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.provider; package ca.uhn.fhir.jpa.provider;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse;
public class BaseJpaResourceProviderQuestionnaireResponseDstu2 extends JpaResourceProviderDstu2<QuestionnaireResponse> { public class BaseJpaResourceProviderQuestionnaireResponseDstu2 extends JpaResourceProviderDstu2<QuestionnaireResponse> {

View File

@ -1,116 +1,76 @@
package ca.uhn.fhir.jpa.provider; package ca.uhn.fhir.jpa.provider;
/* import static org.apache.commons.lang3.StringUtils.isNotBlank;
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.ValueSet; import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept; import ca.uhn.fhir.model.primitive.BooleanDt;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeInclude; import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeIncludeConcept;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParam;
public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDstu2<ValueSet> { public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDstu2<ValueSet> {
//@formatter:off
@Operation(name = "$expand", idempotent = true) @Operation(name = "$expand", idempotent = true)
public ValueSet everything(HttpServletRequest theServletRequest, @IdParam IdDt theId, @OperationParam(name = "filter") StringDt theFilter) { public ValueSet everything(
HttpServletRequest theServletRequest,
@IdParam IdDt theId,
@OperationParam(name = "filter") StringDt theFilter) {
//@formatter:on
startRequest(theServletRequest); startRequest(theServletRequest);
try { try {
ValueSet retVal = new ValueSet(); IFhirResourceDaoValueSet<ValueSet> dao = (IFhirResourceDaoValueSet<ValueSet>) getDao();
retVal.setDate(DateTimeDt.withCurrentTime()); return dao.expand(theId, theFilter);
ValueSet source = read(theServletRequest, theId);
Map<String, ComposeInclude> systemToCompose = new HashMap<String, ComposeInclude>();
/*
* Add composed concepts
*/
for (ComposeInclude nextInclude : source.getCompose().getInclude()) {
for (ComposeIncludeConcept next : nextInclude.getConcept()) {
ComposeInclude include = null;
if (theFilter == null || theFilter.isEmpty()) {
if (include == null) {
include = getOrAddComposeInclude(retVal, systemToCompose, nextInclude.getSystem());
}
include.addConcept(next);
} else {
String filter = theFilter.getValue().toLowerCase();
if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
if (include == null) {
include = getOrAddComposeInclude(retVal, systemToCompose, nextInclude.getSystem());
}
include.addConcept(next);
}
}
}
}
/*
* Add defined concepts
*/
ComposeInclude include = null;
for (CodeSystemConcept next : source.getCodeSystem().getConcept()) {
if (theFilter == null || theFilter.isEmpty()) {
if (include == null) {
include = getOrAddComposeInclude(retVal, systemToCompose, source.getCodeSystem().getSystem());
}
include.addConcept(new ComposeIncludeConcept().setCode(next.getCode()).setDisplay(next.getDisplay()));
} else {
String filter = theFilter.getValue().toLowerCase();
if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
if (include == null) {
include = getOrAddComposeInclude(retVal, systemToCompose, source.getCodeSystem().getSystem());
}
include.addConcept(new ComposeIncludeConcept().setCode(next.getCode()).setDisplay(next.getDisplay()));
}
}
}
return retVal;
} finally { } finally {
endRequest(theServletRequest); endRequest(theServletRequest);
} }
} }
private ComposeInclude getOrAddComposeInclude(ValueSet retVal, Map<String, ComposeInclude> systemToCompose, String system) { //@formatter:off
ComposeInclude include; @Operation(name = "$validate-code", idempotent = true, returnParameters= {
include = systemToCompose.get(system); @OperationParam(name="result", type=BooleanDt.class, min=1),
if (include == null) { @OperationParam(name="message", type=StringDt.class),
include = retVal.getCompose().addInclude(); @OperationParam(name="display", type=StringDt.class)
include.setSystem(system); })
systemToCompose.put(system, include); public Parameters everything(
HttpServletRequest theServletRequest,
@IdParam IdDt theId,
@OperationParam(name="identifier") UriDt theValueSetIdentifier,
@OperationParam(name="code") CodeDt theCode,
@OperationParam(name="system") UriDt theSystem,
@OperationParam(name="display") StringDt theDisplay,
@OperationParam(name="coding") CodingDt theCoding,
@OperationParam(name="codeableConcept") CodeableConceptDt theCodeableConcept
) {
//@formatter:on
startRequest(theServletRequest);
try {
IFhirResourceDaoValueSet<ValueSet> dao = (IFhirResourceDaoValueSet<ValueSet>) getDao();
ValidateCodeResult result = dao.validateCode(theValueSetIdentifier, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept);
Parameters retVal = new Parameters();
retVal.addParameter().setName("result").setValue(new BooleanDt(result.isResult()));
if (isNotBlank(result.getMessage())) {
retVal.addParameter().setName("message").setValue(new StringDt(result.getMessage()));
}
if (isNotBlank(result.getDisplay())) {
retVal.addParameter().setName("display").setValue(new StringDt(result.getDisplay()));
}
return retVal;
} finally {
endRequest(theServletRequest);
} }
return include;
} }
} }

View File

@ -0,0 +1,217 @@
package ca.uhn.fhir.jpa.dao;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.*;
import java.io.IOException;
import java.io.InputStreamReader;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult;
import ca.uhn.fhir.jpa.provider.ResourceProviderDstu2Test;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "/hapi-fhir-server-resourceproviders-dstu2.xml", "/fhir-jpabase-spring-test-config.xml" })
public class FhirResourceDaoValueSetDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoValueSetDstu2Test.class);
private static IIdType vsid;
@Autowired
private FhirContext myCtx;
@Autowired
@Qualifier("mySystemDaoDstu2")
private IFhirSystemDao<Bundle> mySystemDao;
@Autowired
@Qualifier("myValueSetDaoDstu2")
private IFhirResourceDaoValueSet<ValueSet> myValueSetDao;
@Before
@Transactional
public void before01() {
if (vsid == null) {
FhirSystemDaoDstu2Test.doDeleteEverything(mySystemDao);
}
}
@Before
@Transactional
public void before02() {
if (vsid == null) {
ValueSet upload = myCtx.newXmlParser().parseResource(ValueSet.class, new InputStreamReader(ResourceProviderDstu2Test.class.getResourceAsStream("/extensional-case-2.xml")));
upload.setId("");
vsid = myValueSetDao.create(upload).getId().toUnqualifiedVersionless();
}
}
@Test
public void testValidateCodeOperationByCodeAndSystemBad() {
UriDt valueSetIdentifier = null;
IdDt id = null;
CodeDt code = new CodeDt("8450-9-XXX");
UriDt system = new UriDt("http://loinc.org");
StringDt display = null;
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertFalse(result.isResult());
}
@Test
public void testValidateCodeOperationByCodeAndSystemGood() {
UriDt valueSetIdentifier = null;
IdDt id = null;
CodeDt code = new CodeDt("8450-9");
UriDt system = new UriDt("http://loinc.org");
StringDt display = null;
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertTrue(result.isResult());
assertEquals("Systolic blood pressure--expiration", result.getDisplay());
}
@Test
public void testValidateCodeOperationByIdentifierAndCodeAndSystem() {
UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2");
IdDt id = null;
CodeDt code = new CodeDt("11378-7");
UriDt system = new UriDt("http://loinc.org");
StringDt display = null;
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertTrue(result.isResult());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}
@Test
public void testValidateCodeOperationByIdentifierAndCodeAndSystemAndBadDisplay() {
UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2");
IdDt id = null;
CodeDt code = new CodeDt("11378-7");
UriDt system = new UriDt("http://loinc.org");
StringDt display = new StringDt("Systolic blood pressure at First encounterXXXX");
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertFalse(result.isResult());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}
@Test
public void testValidateCodeOperationByIdentifierAndCodeAndSystemAndGoodDisplay() {
UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2");
IdDt id = null;
CodeDt code = new CodeDt("11378-7");
UriDt system = new UriDt("http://loinc.org");
StringDt display = new StringDt("Systolic blood pressure at First encounter");
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertTrue(result.isResult());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}
@Test
public void testValidateCodeOperationByResourceIdAndCodeableConcept() {
UriDt valueSetIdentifier = null;
IIdType id = vsid;
CodeDt code = null;
UriDt system = null;
StringDt display = null;
CodingDt coding = null;
CodeableConceptDt codeableConcept = new CodeableConceptDt("http://loinc.org", "11378-7");
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertTrue(result.isResult());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}
@Test
public void testValidateCodeOperationByResourceIdAndCodeAndSystem() {
UriDt valueSetIdentifier = null;
IIdType id = vsid;
CodeDt code = new CodeDt("11378-7");
UriDt system = new UriDt("http://loinc.org");
StringDt display = null;
CodingDt coding = null;
CodeableConceptDt codeableConcept = null;
ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept);
assertTrue(result.isResult());
assertEquals("Systolic blood pressure at First encounter", result.getDisplay());
}
@Test
public void testValueSetExpandOperation() throws IOException {
String resp;
ValueSet expanded = myValueSetDao.expand(vsid, null);
resp = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded);
ourLog.info(resp);
// @formatter:off
assertThat(resp,
stringContainsInOrder("<ValueSet xmlns=\"http://hl7.org/fhir\">",
"<expansion>",
"<contains>",
"<system value=\"http://loinc.org\"/>",
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>",
"</contains>",
"<contains>",
"<system value=\"http://loinc.org\"/>",
"<code value=\"8450-9\"/>",
"<display value=\"Systolic blood pressure--expiration\"/>",
"</contains>",
"</expansion>"
));
//@formatter:on
/*
* Filter with display name
*/
expanded = myValueSetDao.expand(vsid, new StringDt("systolic"));
resp = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded);
ourLog.info(resp);
//@formatter:off
assertThat(resp, stringContainsInOrder(
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>"));
//@formatter:on
/*
* Filter with code
*/
expanded = myValueSetDao.expand(vsid, new StringDt("11378"));
resp = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded);
ourLog.info(resp);
//@formatter:off
assertThat(resp, stringContainsInOrder(
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>"));
//@formatter:on
}
}

View File

@ -428,8 +428,6 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
@Test @Test
public void testCreateQuestionnaireResponseWithValidation() throws IOException { public void testCreateQuestionnaireResponseWithValidation() throws IOException {
String methodName = "testCreateQuestionnaireResponseWithValidation";
ValueSet options = new ValueSet(); ValueSet options = new ValueSet();
options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0"); options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
IIdType optId = ourClient.create().resource(options).execute().getId(); IIdType optId = ourClient.create().resource(options).execute().getId();
@ -1508,21 +1506,21 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
ourLog.info(resp); ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
// @formatter:off // @formatter:off
assertThat( assertThat(resp,
resp, stringContainsInOrder("<ValueSet xmlns=\"http://hl7.org/fhir\">",
stringContainsInOrder("<ValueSet xmlns=\"http://hl7.org/fhir\">", "<expansion>",
"<compose>", "<contains>",
"<include>",
"<system value=\"http://loinc.org\"/>", "<system value=\"http://loinc.org\"/>",
"<concept>",
"<code value=\"11378-7\"/>", "<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>", "<display value=\"Systolic blood pressure at First encounter\"/>",
"</concept>", "</contains>",
"<concept>", "<contains>",
"<system value=\"http://loinc.org\"/>",
"<code value=\"8450-9\"/>", "<code value=\"8450-9\"/>",
"<display value=\"Systolic blood pressure--expiration\"/>", "<display value=\"Systolic blood pressure--expiration\"/>",
"</concept>" "</contains>",
)); "</expansion>"
));
//@formatter:on //@formatter:on
} finally { } finally {
IOUtils.closeQuietly(response.getEntity().getContent()); IOUtils.closeQuietly(response.getEntity().getContent());

View File

@ -28,7 +28,8 @@
#foreach ( $res in $resources ) #foreach ( $res in $resources )
<bean id="my${res.name}Dao${versionCapitalized}" <bean id="my${res.name}Dao${versionCapitalized}"
#if ( ${versionCapitalized} == 'Dstu2' && ( ${res.name} == 'Bundle' || ${res.name} == 'QuestionnaireResponse' )) ## Some resource types have customized DAOs for resource specific functionality
#if ( ${versionCapitalized} == 'Dstu2' && ( ${res.name} == 'Bundle' || ${res.name} == 'QuestionnaireResponse' || ${res.name} == 'ValueSet'))
class="ca.uhn.fhir.jpa.dao.FhirResourceDao${res.name}${versionCapitalized}"> class="ca.uhn.fhir.jpa.dao.FhirResourceDao${res.name}${versionCapitalized}">
#else #else
class="ca.uhn.fhir.jpa.dao.FhirResourceDao${versionCapitalized}"> class="ca.uhn.fhir.jpa.dao.FhirResourceDao${versionCapitalized}">

View File

@ -336,6 +336,11 @@
<artifactId>spring-orm</artifactId> <artifactId>spring-orm</artifactId>
<version>${spring_version}</version> <version>${spring_version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId> <artifactId>spring-tx</artifactId>

View File

@ -93,6 +93,9 @@
a resourceType element) fail to parse with a confusing NullPointerException. a resourceType element) fail to parse with a confusing NullPointerException.
Thanks to GitHub user @hugosoares for reporting! Thanks to GitHub user @hugosoares for reporting!
</action> </action>
<action type="add">
JPA server now implements the $validate-code operation
</action>
</release> </release>
<release version="1.1" date="2015-07-13"> <release version="1.1" date="2015-07-13">
<action type="add"> <action type="add">