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