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}