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.Arrays;
037import java.util.List;
038import java.util.Optional;
039import java.util.stream.Collectors;
040import java.util.stream.Stream;
041
042import static ca.uhn.fhir.util.ObjectUtil.castIfInstanceof;
043import static org.apache.commons.lang3.StringUtils.isBlank;
044
045public class ResourceCompartmentUtil {
046
047        /**
048         * Extract, if exists, the patient compartment identity of the received resource.
049         * It must be invoked in patient compartment mode.
050         * @param theResource             the resource to which extract the patient compartment identity
051         * @param theFhirContext          the active FhirContext
052         * @param theSearchParamExtractor the configured search parameter extractor
053         * @return the optional patient compartment identifier
054         * @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned.
055         */
056        public static Optional<String> getPatientCompartmentIdentity(
057                        IBaseResource theResource, FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) {
058                if (theResource == null) {
059                        // The resource may be null in mass ingestion mode
060                        return Optional.empty();
061                }
062
063                RuntimeResourceDefinition resourceDef = theFhirContext.getResourceDefinition(theResource);
064                List<RuntimeSearchParam> patientCompartmentSps =
065                                ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef);
066                if (patientCompartmentSps.isEmpty()) {
067                        return Optional.empty();
068                }
069
070                if (resourceDef.getName().equals("Patient")) {
071                        String compartmentIdentity = theResource.getIdElement().getIdPart();
072                        if (isBlank(compartmentIdentity)) {
073                                throw new MethodNotAllowedException(
074                                                Msg.code(2475)
075                                                                + "Patient resource IDs must be client-assigned in patient compartment mode, or server id strategy must be UUID");
076                        }
077                        return Optional.of(compartmentIdentity);
078                }
079
080                return getResourceCompartment("Patient", theResource, patientCompartmentSps, theSearchParamExtractor);
081        }
082
083        /**
084         * Extracts and returns an optional compartment of the received resource
085         * @param theCompartmentName     the name of the compartment
086         * @param theResource            source resource which compartment is extracted
087         * @param theCompartmentSps      the RuntimeSearchParam list involving the searched compartment
088         * @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values
089         * @return optional compartment of the received resource
090         */
091        public static Optional<String> getResourceCompartment(
092                        String theCompartmentName,
093                        IBaseResource theResource,
094                        List<RuntimeSearchParam> theCompartmentSps,
095                        ISearchParamExtractor mySearchParamExtractor) {
096                // TODO KHS consolidate with FhirTerser.getCompartmentOwnersForResource()
097                return getResourceCompartmentReferences(theResource, theCompartmentSps, mySearchParamExtractor)
098                                .map(t -> t.getReferenceElement().getValue())
099                                .map(IdType::new)
100                                .filter(t -> theCompartmentName.equals(
101                                                t.getResourceType())) // assume the compartment name matches the resource type
102                                .map(IdType::getIdPart)
103                                .filter(StringUtils::isNotBlank)
104                                .findFirst();
105        }
106
107        @Nonnull
108        public static Stream<IBaseReference> getResourceCompartmentReferences(
109                        IBaseResource theResource,
110                        List<RuntimeSearchParam> theCompartmentSps,
111                        ISearchParamExtractor mySearchParamExtractor) {
112                return theCompartmentSps.stream()
113                                .flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
114                                .filter(StringUtils::isNotBlank)
115                                .flatMap(path -> mySearchParamExtractor.getPathValueExtractor(theResource, path).get().stream())
116                                .flatMap(base -> castIfInstanceof(base, IBaseReference.class).stream());
117        }
118
119        /**
120         * Returns a {@code RuntimeSearchParam} list with the parameters extracted from the received
121         * {@code RuntimeResourceDefinition}, which are of type REFERENCE and have a membership compartment
122         * for "Patient" resource
123         * @param resourceDef the RuntimeResourceDefinition providing the RuntimeSearchParam list
124         * @return the RuntimeSearchParam filtered list
125         */
126        @Nonnull
127        public static List<RuntimeSearchParam> getPatientCompartmentSearchParams(RuntimeResourceDefinition resourceDef) {
128                return resourceDef.getSearchParams().stream()
129                                .filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
130                                .filter(param -> param.getProvidesMembershipInCompartments() != null
131                                                && param.getProvidesMembershipInCompartments().contains("Patient"))
132                                .collect(Collectors.toList());
133        }
134}