
001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.provider; 021 022import ca.uhn.fhir.context.support.TranslateConceptResults; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap; 025import ca.uhn.fhir.jpa.api.model.TranslationRequest; 026import ca.uhn.fhir.jpa.model.util.JpaConstants; 027import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl; 028import ca.uhn.fhir.rest.annotation.IdParam; 029import ca.uhn.fhir.rest.annotation.Operation; 030import ca.uhn.fhir.rest.annotation.OperationParam; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 033import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 034import jakarta.servlet.http.HttpServletRequest; 035import org.hl7.fhir.instance.model.api.IBase; 036import org.hl7.fhir.instance.model.api.IBaseCoding; 037import org.hl7.fhir.instance.model.api.IBaseDatatype; 038import org.hl7.fhir.instance.model.api.IBaseParameters; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IIdType; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042import org.hl7.fhir.r4.model.CodeableConcept; 043import org.hl7.fhir.r4.model.Coding; 044import org.hl7.fhir.r4.model.ConceptMap; 045import org.hl7.fhir.r4.model.Parameters; 046import org.springframework.beans.factory.annotation.Autowired; 047 048import static ca.uhn.fhir.util.DatatypeUtil.toBooleanValue; 049import static ca.uhn.fhir.util.DatatypeUtil.toStringValue; 050import static org.apache.commons.lang3.StringUtils.isNotBlank; 051 052public abstract class BaseJpaResourceProviderConceptMap<T extends IBaseResource> extends BaseJpaResourceProvider<T> { 053 054 @Autowired 055 private VersionCanonicalizer myVersionCanonicalizer; 056 057 /** 058 * Note: Several parameters for the $translate operation were renamed between FHIR R4 and R5. 059 * To be kind to implementers, we support both names on any version of FHIR, although 060 * we will error out if someone tries to use both. 061 * <p> 062 * This is a messy solution: It means that we will advertise both sets of parameters in the 063 * CapabilityStatement, and that an R5 client can use an R4 parameter without getting an error. 064 * This seems like a good tradeoff though, since it makes the API easier to use when you are 065 * trying to upgrade FHIR versions. In future maybe we could make this configurable? 066 */ 067 @Operation( 068 name = JpaConstants.OPERATION_TRANSLATE, 069 idempotent = true, 070 returnParameters = { 071 @OperationParam(name = "result", typeName = "boolean", min = 1, max = 1), 072 @OperationParam(name = "message", typeName = "string", min = 0, max = 1), 073 }) 074 public IBaseParameters translate( 075 HttpServletRequest theServletRequest, 076 @IdParam(optional = true) IIdType theId, 077 @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theUrl, 078 @OperationParam(name = "conceptMapVersion", min = 0, max = 1, typeName = "string") 079 IPrimitiveType<String> theConceptMapVersion, 080 @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") 081 IPrimitiveType<String> theSourceCodeSystem, 082 @OperationParam(name = "version", min = 0, max = 1, typeName = "string") 083 IPrimitiveType<String> theSourceCodeSystemVersion, 084 085 // R4 (and below) source ValueSet 086 @OperationParam(name = "source", min = 0, max = 1, typeName = "uri") 087 IPrimitiveType<String> theSourceValueSetR4, 088 // R5+ source ValueSet 089 @OperationParam(name = "sourceScope", min = 0, max = 1, typeName = "uri") 090 IPrimitiveType<String> theSourceValueSetR5, 091 092 // R4 (and below) source parameters 093 @OperationParam(name = "code", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theSourceCodeR4, 094 @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theSourceCodingR4, 095 @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept") 096 IBaseDatatype theSourceCodeableConceptR4, 097 // R5+ source parameters 098 @OperationParam(name = "sourceCode", min = 0, max = 1, typeName = "code") 099 IPrimitiveType<String> theSourceCodeR5, 100 @OperationParam(name = "sourceCoding", min = 0, max = 1, typeName = "Coding") IBaseCoding theSourceCodingR5, 101 @OperationParam(name = "sourceCodeableConcept", min = 0, max = 1, typeName = "CodeableConcept") 102 IBaseDatatype theSourceCodeableConceptR5, 103 104 // R4 (and below) target ValueSet 105 @OperationParam(name = "target", min = 0, max = 1, typeName = "uri") 106 IPrimitiveType<String> theTargetValueSetR4, 107 // R5+ target ValueSet 108 @OperationParam(name = "targetScope", min = 0, max = 1, typeName = "uri") 109 IPrimitiveType<String> theTargetValueSetR5, 110 @OperationParam(name = "targetsystem", min = 0, max = 1, typeName = "uri") 111 IPrimitiveType<String> theTargetCodeSystemR4, 112 @OperationParam(name = "targetSystem", min = 0, max = 1, typeName = "uri") 113 IPrimitiveType<String> theTargetCodeSystemR5, 114 @OperationParam(name = "reverse", min = 0, max = 1, typeName = "boolean") 115 IPrimitiveType<Boolean> theReverse, 116 RequestDetails theRequestDetails) { 117 118 Coding sourceCoding = myVersionCanonicalizer.codingToCanonical( 119 pickOne("coding", theSourceCodingR4, "sourceCoding", theSourceCodingR5)); 120 121 CodeableConcept sourceCodeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(pickOne( 122 "codeableConcept", theSourceCodeableConceptR4, "sourceCodeableConcept", theSourceCodeableConceptR5)); 123 124 IPrimitiveType<String> sourceCode = pickOne("code", theSourceCodeR4, "sourceCode", theSourceCodeR5); 125 126 boolean haveSourceCode = sourceCode != null && isNotBlank(sourceCode.getValue()); 127 boolean haveSourceCodeSystem = theSourceCodeSystem != null && theSourceCodeSystem.hasValue(); 128 boolean haveSourceCodeSystemVersion = 129 theSourceCodeSystemVersion != null && theSourceCodeSystemVersion.hasValue(); 130 boolean haveSourceCoding = sourceCoding != null && sourceCoding.hasCode(); 131 boolean haveSourceCodeableConcept = sourceCodeableConcept != null 132 && sourceCodeableConcept.hasCoding() 133 && sourceCodeableConcept.getCodingFirstRep().hasCode(); 134 boolean haveReverse = theReverse != null; 135 boolean haveId = theId != null && theId.hasIdPart(); 136 137 // <editor-fold desc="Filters"> 138 if ((!haveSourceCode && !haveSourceCoding && !haveSourceCodeableConcept) 139 || moreThanOneTrue(haveSourceCode, haveSourceCoding, haveSourceCodeableConcept)) { 140 throw new InvalidRequestException( 141 Msg.code(1154) 142 + "One (and only one) of the in parameters (code, coding, codeableConcept) must be provided, to identify the code that is to be translated."); 143 } 144 145 TranslationRequest translationRequest = new TranslationRequest(); 146 translationRequest.setUrl(toStringValue(theUrl)); 147 translationRequest.setConceptMapVersion(toStringValue(theConceptMapVersion)); 148 149 if (haveSourceCode) { 150 translationRequest.getCodeableConcept().addCoding().setCode(toStringValue(sourceCode)); 151 152 if (haveSourceCodeSystem) { 153 translationRequest 154 .getCodeableConcept() 155 .getCodingFirstRep() 156 .setSystem(toStringValue(theSourceCodeSystem)); 157 } 158 159 if (haveSourceCodeSystemVersion) { 160 translationRequest 161 .getCodeableConcept() 162 .getCodingFirstRep() 163 .setVersion(toStringValue(theSourceCodeSystemVersion)); 164 } 165 } else if (haveSourceCoding) { 166 translationRequest.getCodeableConcept().addCoding(sourceCoding); 167 } else { 168 translationRequest.setCodeableConcept(sourceCodeableConcept); 169 } 170 171 translationRequest.setSource( 172 toStringValue(pickOne("source", theSourceValueSetR4, "sourceScope", theSourceValueSetR5))); 173 translationRequest.setTarget( 174 toStringValue(pickOne("target", theTargetValueSetR4, "targetScope", theTargetValueSetR5))); 175 translationRequest.setTargetSystem( 176 toStringValue(pickOne("targetsystem", theTargetCodeSystemR4, "targetSystem", theTargetCodeSystemR5))); 177 178 if (haveReverse) { 179 translationRequest.setReverse(toBooleanValue(theReverse)); 180 } 181 182 if (haveId) { 183 translationRequest.setResourceId(theId); 184 } 185 186 startRequest(theServletRequest); 187 try { 188 IFhirResourceDaoConceptMap<ConceptMap> dao = (IFhirResourceDaoConceptMap<ConceptMap>) getDao(); 189 TranslateConceptResults result = dao.translate(translationRequest, theRequestDetails); 190 Parameters parameters = TermConceptMappingSvcImpl.toParameters(result); 191 return myVersionCanonicalizer.parametersFromCanonical(parameters); 192 } finally { 193 endRequest(theServletRequest); 194 } 195 } 196 197 private static <T extends IBase> T pickOne(String theR4Name, T theR4Value, String theR5Name, T theR5Value) { 198 if (theR4Value != null && theR5Value != null) { 199 throw new InvalidRequestException(Msg.code(2805) + "Can't combine the $translate R4 parameter '" + theR4Name 200 + "' with the R5 parameter '" + theR5Name + "'"); 201 } 202 if (theR4Value != null && !theR4Value.isEmpty()) { 203 return theR4Value; 204 } 205 if (theR5Value != null && !theR5Value.isEmpty()) { 206 return theR4Value; 207 } 208 return null; 209 } 210 211 private static boolean moreThanOneTrue(boolean... theBooleans) { 212 boolean haveOne = false; 213 for (boolean next : theBooleans) { 214 if (next) { 215 if (haveOne) { 216 return true; 217 } else { 218 haveOne = true; 219 } 220 } 221 } 222 return false; 223 } 224}