001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.util;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
027import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
028import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
029import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
030import jakarta.annotation.Nonnull;
031import org.apache.commons.lang3.StringUtils;
032import org.hl7.fhir.instance.model.api.IBaseReference;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.r4.model.IdType;
035
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Optional;
041import java.util.Set;
042import java.util.stream.Stream;
043
044import static ca.uhn.fhir.util.ObjectUtil.castIfInstanceof;
045import static org.apache.commons.lang3.StringUtils.isBlank;
046
047public class ResourceCompartmentUtil {
048
049        /**
050         * Extract, if exists, the patient compartment identity of the received resource.
051         * It must be invoked in patient compartment mode.
052         * @param theResource             the resource to which extract the patient compartment identity
053         * @param theFhirContext          the active FhirContext
054         * @param theSearchParamExtractor the configured search parameter extractor
055         * @return the optional patient compartment identifier
056         * @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned.
057         */
058        public static Optional<String> getPatientCompartmentIdentity(
059                        IBaseResource theResource, FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) {
060                if (theResource == null) {
061                        // The resource may be null in mass ingestion mode
062                        return Optional.empty();
063                }
064
065                RuntimeResourceDefinition resourceDef = theFhirContext.getResourceDefinition(theResource);
066                List<RuntimeSearchParam> patientCompartmentSps =
067                                ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef);
068                if (patientCompartmentSps.isEmpty()) {
069                        return Optional.empty();
070                }
071
072                if (resourceDef.getName().equals("Patient")) {
073                        String compartmentIdentity = theResource.getIdElement().getIdPart();
074                        if (isBlank(compartmentIdentity)) {
075                                throw new MethodNotAllowedException(
076                                                Msg.code(2475)
077                                                                + "Patient resource IDs must be client-assigned in patient compartment mode, or server id strategy must be UUID");
078                        }
079                        return Optional.of(compartmentIdentity);
080                }
081
082                return getResourceCompartment("Patient", theResource, patientCompartmentSps, theSearchParamExtractor);
083        }
084
085        /**
086         * Extracts and returns an optional compartment of the received resource
087         * @param theCompartmentName     the name of the compartment
088         * @param theResource            source resource which compartment is extracted
089         * @param theCompartmentSps      the RuntimeSearchParam list involving the searched compartment
090         * @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values
091         * @return optional compartment of the received resource
092         */
093        public static Optional<String> getResourceCompartment(
094                        String theCompartmentName,
095                        IBaseResource theResource,
096                        List<RuntimeSearchParam> theCompartmentSps,
097                        ISearchParamExtractor mySearchParamExtractor) {
098                // TODO KHS consolidate with FhirTerser.getCompartmentOwnersForResource()
099                return getResourceCompartmentReferences(theResource, theCompartmentSps, mySearchParamExtractor)
100                                .map(t -> t.getReferenceElement().getValue())
101                                .map(IdType::new)
102                                .filter(t -> theCompartmentName.equals(
103                                                t.getResourceType())) // assume the compartment name matches the resource type
104                                .map(IdType::getIdPart)
105                                .filter(StringUtils::isNotBlank)
106                                .findFirst();
107        }
108
109        @Nonnull
110        public static Stream<IBaseReference> getResourceCompartmentReferences(
111                        IBaseResource theResource,
112                        List<RuntimeSearchParam> theCompartmentSps,
113                        ISearchParamExtractor mySearchParamExtractor) {
114                return theCompartmentSps.stream()
115                                .flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
116                                .filter(StringUtils::isNotBlank)
117                                .flatMap(path -> mySearchParamExtractor.getPathValueExtractor(theResource, path).get().stream())
118                                .flatMap(base -> castIfInstanceof(base, IBaseReference.class).stream());
119        }
120
121        /**
122         * Returns a {@code RuntimeSearchParam} list with the parameters extracted from the received
123         * {@code RuntimeResourceDefinition}, which are of type REFERENCE and have a membership compartment
124         * for "Patient" resource
125         *
126         * @param theResourceDef the RuntimeResourceDefinition providing the RuntimeSearchParam list
127         * @return the RuntimeSearchParam filtered list
128         */
129        @Nonnull
130        public static List<RuntimeSearchParam> getPatientCompartmentSearchParams(
131                        @Nonnull RuntimeResourceDefinition theResourceDef) {
132                return getPatientCompartmentSearchParams(theResourceDef, false);
133        }
134
135        /**
136         * Returns a {@code RuntimeSearchParam} list with the parameters extracted from the received
137         * {@code RuntimeResourceDefinition}, which are of type REFERENCE and have a membership compartment
138         * for "Patient" resource
139         *
140         * @param theResourceDef      the RuntimeResourceDefinition providing the RuntimeSearchParam list
141         * @param theIncludeSupersets If <code>false</code>, include only the parameters explicitly defined as being a part
142         *                            of the Patient compartment. If <code>true</code>, include other parameters whose path
143         *                            would include the same resources. For example, for the <code>Observation</code> resource
144         *                            type, the superset would include both the <code>subject</code> and <code>patient</code>
145         *                            parameters, where the non-superset would include only the <code>patient</code> parameter.
146         * @return the RuntimeSearchParam filtered list
147         * @since 8.6.0
148         */
149        @Nonnull
150        public static List<RuntimeSearchParam> getPatientCompartmentSearchParams(
151                        @Nonnull RuntimeResourceDefinition theResourceDef, boolean theIncludeSupersets) {
152                List<RuntimeSearchParam> retVal = new ArrayList<>(3);
153                for (RuntimeSearchParam param : theResourceDef.getSearchParams()) {
154                        if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
155                                if (param.getProvidesMembershipInCompartments() != null
156                                                && param.getProvidesMembershipInCompartments().contains("Patient")) {
157                                        retVal.add(param);
158                                }
159                        }
160                }
161
162                if (theIncludeSupersets) {
163                        Set<String> compartmentPaths = new HashSet<>(retVal.size());
164                        for (RuntimeSearchParam param : retVal) {
165                                compartmentPaths.add(param.getPath());
166                        }
167
168                        for (RuntimeSearchParam candidateParam : theResourceDef.getSearchParams()) {
169                                if (candidateParam.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
170                                        if (!compartmentPaths.contains(candidateParam.getPath())) {
171                                                for (String path : compartmentPaths) {
172                                                        /*
173                                                         * We check both directions because parameters are inconsistently defined in FHIR:
174                                                         * Observation uses subject (less precise than patient)
175                                                         * Encounter uses patient (more precise than subject)
176                                                         */
177                                                        if (candidateParam.getPath().startsWith(path)
178                                                                        || path.startsWith(candidateParam.getPath())) {
179                                                                retVal.add(candidateParam);
180                                                        }
181                                                }
182                                        }
183                                }
184                        }
185                }
186
187                return retVal;
188        }
189}