001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2024 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.searchparam.registry;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
027import ca.uhn.fhir.util.BundleUtil;
028import ca.uhn.fhir.util.ClasspathUtil;
029import jakarta.annotation.Nonnull;
030import jakarta.annotation.Nullable;
031import org.apache.commons.lang3.Validate;
032import org.hl7.fhir.instance.model.api.IBaseBundle;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034
035import java.util.Collection;
036import java.util.Collections;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.stream.Stream;
042
043import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
044
045public class ReadOnlySearchParamCache {
046
047        // resourceName -> searchParamName -> searchparam
048        protected final Map<String, ResourceSearchParams> myResourceNameToSpNameToSp;
049        protected final Map<String, RuntimeSearchParam> myUrlToParam;
050
051        /**
052         * Constructor
053         */
054        ReadOnlySearchParamCache() {
055                myResourceNameToSpNameToSp = new HashMap<>();
056                myUrlToParam = new HashMap<>();
057        }
058
059        /**
060         * Copy constructor
061         */
062        private ReadOnlySearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
063                myResourceNameToSpNameToSp = theRuntimeSearchParamCache.myResourceNameToSpNameToSp;
064                myUrlToParam = theRuntimeSearchParamCache.myUrlToParam;
065        }
066
067        public Stream<RuntimeSearchParam> getSearchParamStream() {
068                return myResourceNameToSpNameToSp.values().stream().flatMap(entry -> entry.values().stream());
069        }
070
071        protected ResourceSearchParams getSearchParamMap(String theResourceName) {
072                ResourceSearchParams retval = myResourceNameToSpNameToSp.get(theResourceName);
073                if (retval == null) {
074                        return ResourceSearchParams.empty(theResourceName);
075                }
076                return retval.readOnly();
077        }
078
079        public int size() {
080                return myResourceNameToSpNameToSp.size();
081        }
082
083        public RuntimeSearchParam getByUrl(String theUrl) {
084                return myUrlToParam.get(theUrl);
085        }
086
087        public static ReadOnlySearchParamCache fromFhirContext(
088                        @Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) {
089                return fromFhirContext(theFhirContext, theCanonicalizer, null);
090        }
091
092        public static ReadOnlySearchParamCache fromFhirContext(
093                        @Nonnull FhirContext theFhirContext,
094                        @Nonnull SearchParameterCanonicalizer theCanonicalizer,
095                        @Nullable Set<String> theSearchParamPatternsToInclude) {
096                assert theCanonicalizer != null;
097
098                ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache();
099
100                Set<String> resourceNames = theFhirContext.getResourceTypes();
101
102                /*
103                 * For R4/R4B we include a Bundle of SearchParameters from the core spec in
104                 * hapi-fhir-validation-resources-r4/r4b
105                 *
106                 * For R5 we include the NPM core R5 packages in
107                 * hapi-fhir-validation-resources-r5, so we get them from it
108                 */
109                List<IBaseResource> searchParams = null;
110                if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R4) {
111                        IBaseBundle allSearchParameterBundle = (IBaseBundle) theFhirContext
112                                        .newJsonParser()
113                                        .parseResource(
114                                                        ClasspathUtil.loadResourceAsStream("org/hl7/fhir/r4/model/sp/search-parameters.json"));
115                        searchParams = BundleUtil.toListOfResources(theFhirContext, allSearchParameterBundle);
116                } else if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R4B) {
117                        IBaseBundle allSearchParameterBundle = (IBaseBundle) theFhirContext
118                                        .newXmlParser()
119                                        .parseResource(
120                                                        ClasspathUtil.loadResourceAsStream("org/hl7/fhir/r4b/model/sp/search-parameters.xml"));
121                        searchParams = BundleUtil.toListOfResources(theFhirContext, allSearchParameterBundle);
122                } else if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R5) {
123                        searchParams = FhirContext.forR5Cached().getValidationSupport().fetchAllSearchParameters();
124                }
125
126                searchParams = defaultIfNull(searchParams, Collections.emptyList());
127                for (IBaseResource next : searchParams) {
128                        RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next);
129
130                        if (nextCanonical != null) {
131
132                                // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with
133                                // a status of DRAFT which means the server doesn't actually apply them.
134                                // At least this was the case as of 2021-12-24 - JA
135                                nextCanonical = new RuntimeSearchParam(
136                                                nextCanonical.getId(),
137                                                nextCanonical.getUri(),
138                                                nextCanonical.getName(),
139                                                nextCanonical.getDescription(),
140                                                nextCanonical.getPath(),
141                                                nextCanonical.getParamType(),
142                                                nextCanonical.getProvidesMembershipInCompartments(),
143                                                nextCanonical.getTargets(),
144                                                RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE,
145                                                nextCanonical.getComboSearchParamType(),
146                                                nextCanonical.getComponents(),
147                                                nextCanonical.getBase());
148
149                                Collection<String> base = nextCanonical.getBase();
150                                if (base.contains("Resource") || base.contains("DomainResource")) {
151                                        base = resourceNames;
152                                }
153
154                                // Add it to our return value if permitted by the pattern parameters
155                                for (String nextResourceName : base) {
156                                        ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent(
157                                                        nextResourceName, t -> new ResourceSearchParams(nextResourceName));
158                                        String nextParamName = nextCanonical.getName();
159                                        if (theSearchParamPatternsToInclude == null
160                                                        || searchParamMatchesAtLeastOnePattern(
161                                                                        theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
162                                                resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextCanonical);
163                                        }
164                                }
165                        }
166                }
167
168                // Now grab all the runtime search parameters from the resource definitions
169                for (String resourceName : resourceNames) {
170                        RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
171                        String nextResourceName = nextResDef.getName();
172
173                        ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent(
174                                        nextResourceName, t -> new ResourceSearchParams(nextResourceName));
175                        for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
176                                String nextParamName = nextSp.getName();
177                                // Add it to our return value if permitted by the pattern parameters
178                                if (theSearchParamPatternsToInclude == null
179                                                || searchParamMatchesAtLeastOnePattern(
180                                                                theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
181                                        resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextSp);
182                                }
183                        }
184                }
185                return retVal;
186        }
187
188        public static boolean searchParamMatchesAtLeastOnePattern(
189                        Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) {
190                for (String nextPattern : theSearchParamPatterns) {
191                        if ("*".equals(nextPattern)) {
192                                return true;
193                        }
194                        int colonIdx = nextPattern.indexOf(':');
195                        Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern);
196                        String resourceType = nextPattern.substring(0, colonIdx);
197                        String searchParamName = nextPattern.substring(colonIdx + 1);
198                        Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern);
199                        Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern);
200                        if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) {
201                                continue;
202                        }
203                        if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) {
204                                continue;
205                        }
206                        return true;
207                }
208
209                return false;
210        }
211
212        public static ReadOnlySearchParamCache fromRuntimeSearchParamCache(
213                        RuntimeSearchParamCache theRuntimeSearchParamCache) {
214                return new ReadOnlySearchParamCache(theRuntimeSearchParamCache);
215        }
216}