diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/MarkdownDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/MarkdownDt.java index 0190d989b46..05254e810d4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/MarkdownDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/MarkdownDt.java @@ -1,5 +1,25 @@ 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 net.sourceforge.cobertura.CoverageIgnore; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index 82b07d87a5e..ed204bbd8ce 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -59,16 +59,16 @@ public @interface OperationParam { Class 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; /** * 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 - * repetitions. + * repetitions (default is 1) */ - int max() default MAX_UNLIMITED; + int max() default 1; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BearerTokenAuthInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BearerTokenAuthInterceptor.java index e915be918b1..ccf0a73f473 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BearerTokenAuthInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BearerTokenAuthInterceptor.java @@ -1,5 +1,25 @@ 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.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 817730b0d4b..dea3993dd2c 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -174,6 +174,10 @@ org.springframework spring-context-support + + org.springframework + spring-test + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 65b159e2646..0f31e6412af 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -19,8 +19,8 @@ package ca.uhn.fhir.jpa.dao; * limitations under the License. * #L% */ - -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.UnsupportedEncodingException; 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.util.FhirTerser; -public abstract class BaseHapiFhirDao implements IDao { +public abstract class BaseHapiFhirDao implements IDao { 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); @@ -344,7 +344,7 @@ public abstract class BaseHapiFhirDao implements IDao { } @SuppressWarnings("unchecked") - protected IFhirResourceDao getDao(Class theType) { + protected IFhirResourceDao getDao(Class theType) { if (myResourceTypeToDao == null) { myResourceTypeToDao = new HashMap, IFhirResourceDao>(); for (IFhirResourceDao next : myResourceDaos) { @@ -358,7 +358,7 @@ public abstract class BaseHapiFhirDao implements IDao { } - return (IFhirResourceDao) myResourceTypeToDao.get(theType); + return (IFhirResourceDao) myResourceTypeToDao.get(theType); } protected TagDefinition getTag(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { @@ -644,12 +644,12 @@ public abstract class BaseHapiFhirDao implements IDao { } - protected Set processMatchUrl(String theMatchUrl, Class theResourceType) { + protected Set processMatchUrl(String theMatchUrl, Class theResourceType) { RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType); SearchParameterMap paramMap = translateMatchUrl(theMatchUrl, resourceDef); - IFhirResourceDao dao = getDao(theResourceType); + IFhirResourceDao dao = getDao(theResourceType); Set ids = dao.searchForIdsWithAndOr(paramMap); return ids; @@ -899,6 +899,7 @@ public abstract class BaseHapiFhirDao implements IDao { case DSTU1: mySearchParamExtractor = new SearchParamExtractorDstu1(theContext); break; + case DSTU2_HL7ORG: case DEV: 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); } - protected T toResource(Class theResourceType, BaseHasResource theEntity) { + protected R toResource(Class theResourceType, BaseHasResource theEntity) { String resourceText = null; switch (theEntity.getEncoding()) { case JSON: @@ -933,7 +934,7 @@ public abstract class BaseHapiFhirDao implements IDao { } IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); - T retVal; + R retVal; try { retVal = parser.parseResource(theResourceType, resourceText); } catch (Exception e) { @@ -955,9 +956,9 @@ public abstract class BaseHapiFhirDao implements IDao { IResource res = (IResource) retVal; res.setId(theEntity.getIdDt()); - res.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, theEntity.getVersion()); - res.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, theEntity.getPublished()); - res.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, theEntity.getUpdated()); + ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); + ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished()); + ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); if (theEntity.getTitle() != null) { ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle()); @@ -1038,9 +1039,10 @@ public abstract class BaseHapiFhirDao implements IDao { 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) { - validateResourceForStorage(theResource); + validateResourceForStorage((T) theResource); if (entity.getPublished() == null) { 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 * The resource that is about to be persisted */ - protected void validateResourceForStorage(IResource theResource) { + protected void validateResourceForStorage(T theResource) { // nothing } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index b865951fe6a..fa16591eec6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -131,7 +131,7 @@ import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ObjectUtil; @Transactional(propagation = Propagation.REQUIRED) -public abstract class BaseHapiFhirResourceDao extends BaseHapiFhirDao implements IFhirResourceDao { +public abstract class BaseHapiFhirResourceDao extends BaseHapiFhirDao implements IFhirResourceDao { static final String OO_SEVERITY_WARN = "warning"; static final String OO_SEVERITY_INFO = "information"; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java index 4ceacc36740..0daa1bc776c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java @@ -1,5 +1,25 @@ 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 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 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.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; @@ -39,7 +58,7 @@ public class FhirResourceDaoQuestionnaireResponseDstu2 extends FhirResourceDaoDs } @Override - protected void validateResourceForStorage(IResource theResource) { + protected void validateResourceForStorage(QuestionnaireResponse theResource) { super.validateResourceForStorage(theResource); if (!myValidateResponses) { return; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java new file mode 100644 index 00000000000..5f292b1710d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -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 FhirResourceDaoDstu2implements IFhirResourceDaoValueSet { + + @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 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 ids = searchForIds(ValueSet.SP_IDENTIFIER, new TokenParam(null, theValueSetIdentifier.getValue())); + valueSetIds = new ArrayList(); + 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 ids = searchForIds(ValueSet.SP_CODE, new TokenParam(toStringOrNull(theSystem), theCode.getValue())); + valueSetIds = new ArrayList(); + for (Long next : ids) { + valueSetIds.add(new IdDt("ValueSet", next)); + } + } + + for (IIdType nextId : valueSetIds) { + ValueSet expansion = expand(nextId, null); + List 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 thePrimitive) { + return thePrimitive != null ? thePrimitive.getValue() : null; + } + + private ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInContains(List 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; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java new file mode 100644 index 00000000000..d415c8ef845 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java @@ -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 extends IFhirResourceDao { + + 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; + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java index 49863dea8b6..c7cfddfe237 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java @@ -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.Communication; 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.primitive.BaseDateTimeDt; import ca.uhn.fhir.model.primitive.IntegerDt; @@ -405,6 +406,12 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen public List extractSearchParamTokens(ResourceTable theEntity, IResource theResource) { ArrayList retVal = new ArrayList(); + String useSystem = null; + if (theResource instanceof ValueSet) { + ValueSet vs = (ValueSet) theResource; + useSystem = vs.getCodeSystem().getSystem(); + } + RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource); for (RuntimeSearchParam nextSpDef : def.getSearchParams()) { if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.TOKEN) { @@ -466,7 +473,11 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen if (nextValue.isEmpty()) { continue; } - systems.add(null); + if ("ValueSet.codeSystem.concept.code".equals(nextPath)) { + systems.add(useSystem); + } else { + systems.add(null); + } codes.add(nextValue.getValueAsString()); } else if (nextObject instanceof CodingDt) { CodingDt nextValue = (CodingDt) nextObject; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderQuestionnaireResponseDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderQuestionnaireResponseDstu2.java index 2c665f85ccb..effd5740a69 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderQuestionnaireResponseDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderQuestionnaireResponseDstu2.java @@ -1,5 +1,25 @@ 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; public class BaseJpaResourceProviderQuestionnaireResponseDstu2 extends JpaResourceProviderDstu2 { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java index f7626b10725..7123da8a607 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java @@ -1,116 +1,76 @@ 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 java.util.HashMap; -import java.util.Map; +import static org.apache.commons.lang3.StringUtils.isNotBlank; 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.CodeSystemConcept; -import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeInclude; -import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeIncludeConcept; -import ca.uhn.fhir.model.primitive.DateTimeDt; +import ca.uhn.fhir.model.primitive.BooleanDt; +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; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDstu2 { + //@formatter:off @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); try { - ValueSet retVal = new ValueSet(); - retVal.setDate(DateTimeDt.withCurrentTime()); - - ValueSet source = read(theServletRequest, theId); - - Map systemToCompose = new HashMap(); - - /* - * 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; - + IFhirResourceDaoValueSet dao = (IFhirResourceDaoValueSet) getDao(); + return dao.expand(theId, theFilter); } finally { endRequest(theServletRequest); } } - private ComposeInclude getOrAddComposeInclude(ValueSet retVal, Map systemToCompose, String system) { - ComposeInclude include; - include = systemToCompose.get(system); - if (include == null) { - include = retVal.getCompose().addInclude(); - include.setSystem(system); - systemToCompose.put(system, include); + //@formatter:off + @Operation(name = "$validate-code", idempotent = true, returnParameters= { + @OperationParam(name="result", type=BooleanDt.class, min=1), + @OperationParam(name="message", type=StringDt.class), + @OperationParam(name="display", type=StringDt.class) + }) + 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 dao = (IFhirResourceDaoValueSet) 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; } - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2Test.java new file mode 100644 index 00000000000..b21849ce836 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2Test.java @@ -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 mySystemDao; + + @Autowired + @Qualifier("myValueSetDaoDstu2") + private IFhirResourceDaoValueSet 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("", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + )); + //@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( + "", + "")); + //@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( + "", + "")); + //@formatter:on + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index 2891babeb2a..1661d9afd08 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -428,8 +428,6 @@ public class ResourceProviderDstu2Test extends BaseJpaTest { @Test public void testCreateQuestionnaireResponseWithValidation() throws IOException { - String methodName = "testCreateQuestionnaireResponseWithValidation"; - ValueSet options = new ValueSet(); options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0"); IIdType optId = ourClient.create().resource(options).execute().getId(); @@ -1508,21 +1506,21 @@ public class ResourceProviderDstu2Test extends BaseJpaTest { ourLog.info(resp); assertEquals(200, response.getStatusLine().getStatusCode()); // @formatter:off - assertThat( - resp, - stringContainsInOrder("", - "", - "", + assertThat(resp, + stringContainsInOrder("", + "", + "", "", - "", "", "", - "", - "", + "", + "", + "", "", "", - "" - )); + "", + "" + )); //@formatter:on } finally { IOUtils.closeQuietly(response.getEntity().getContent()); diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm index 57611d036a2..5e65e4395af 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans.vm @@ -28,7 +28,8 @@ #foreach ( $res in $resources ) #else class="ca.uhn.fhir.jpa.dao.FhirResourceDao${versionCapitalized}"> diff --git a/pom.xml b/pom.xml index 8e3718f1bc6..19c1a7b0006 100644 --- a/pom.xml +++ b/pom.xml @@ -336,6 +336,11 @@ spring-orm ${spring_version} + + org.springframework + spring-test + ${spring_version} + org.springframework spring-tx diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 707bbee1838..60f40dfd65e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -93,6 +93,9 @@ a resourceType element) fail to parse with a confusing NullPointerException. Thanks to GitHub user @hugosoares for reporting! + + JPA server now implements the $validate-code operation +