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        @Nullable
080        public RuntimeSearchParam getByUrl(String theUrl) {
081                return myUrlToParam.get(theUrl);
082        }
083
084        public static ReadOnlySearchParamCache fromFhirContext(
085                        @Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) {
086                return fromFhirContext(theFhirContext, theCanonicalizer, null);
087        }
088
089        public static ReadOnlySearchParamCache fromFhirContext(
090                        @Nonnull FhirContext theFhirContext,
091                        @Nonnull SearchParameterCanonicalizer theCanonicalizer,
092                        @Nullable Set<String> theSearchParamPatternsToInclude) {
093                assert theCanonicalizer != null;
094
095                ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache();
096
097                Set<String> resourceNames = theFhirContext.getResourceTypes();
098
099                // Pull the list of search parameters out of the cached FhirContext
100                // (which avoids us getting a chain containing other JPA validation support providers)
101                FhirContext cachedCtx =
102                                FhirContext.forCached(theFhirContext.getVersion().getVersion());
103                List<IBaseResource> searchParams = cachedCtx.getValidationSupport().fetchAllSearchParameters();
104
105                searchParams = defaultIfNull(searchParams, Collections.emptyList());
106                for (IBaseResource next : searchParams) {
107                        RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next);
108
109                        if (nextCanonical != null) {
110
111                                // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with
112                                // a status of DRAFT which means the server doesn't actually apply them.
113                                // At least this was the case as of 2021-12-24 - JA
114                                nextCanonical = new RuntimeSearchParam(
115                                                nextCanonical.getId(),
116                                                nextCanonical.getUri(),
117                                                nextCanonical.getName(),
118                                                nextCanonical.getDescription(),
119                                                nextCanonical.getPath(),
120                                                nextCanonical.getParamType(),
121                                                nextCanonical.getProvidesMembershipInCompartments(),
122                                                nextCanonical.getTargets(),
123                                                RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE,
124                                                nextCanonical.getComboSearchParamType(),
125                                                nextCanonical.getComponents(),
126                                                nextCanonical.getBase());
127
128                                Collection<String> base = nextCanonical.getBase();
129                                if (base.contains("Resource") || base.contains("DomainResource")) {
130                                        base = resourceNames;
131                                }
132
133                                // Add it to our return value if permitted by the pattern parameters
134                                for (String nextResourceName : base) {
135                                        ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent(
136                                                        nextResourceName, t -> new ResourceSearchParams(nextResourceName));
137                                        String nextParamName = nextCanonical.getName();
138                                        if (theSearchParamPatternsToInclude == null
139                                                        || searchParamMatchesAtLeastOnePattern(
140                                                                        theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
141                                                resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextCanonical);
142                                        }
143                                }
144                        }
145                }
146
147                // Now grab all the runtime search parameters from the resource definitions
148                for (String resourceName : resourceNames) {
149                        RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
150                        String nextResourceName = nextResDef.getName();
151
152                        ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent(
153                                        nextResourceName, t -> new ResourceSearchParams(nextResourceName));
154                        for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
155                                String nextParamName = nextSp.getName();
156                                // Add it to our return value if permitted by the pattern parameters
157                                if (theSearchParamPatternsToInclude == null
158                                                || searchParamMatchesAtLeastOnePattern(
159                                                                theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
160                                        resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextSp);
161                                }
162                        }
163                }
164                return retVal;
165        }
166
167        public static boolean searchParamMatchesAtLeastOnePattern(
168                        Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) {
169                for (String nextPattern : theSearchParamPatterns) {
170                        if ("*".equals(nextPattern)) {
171                                return true;
172                        }
173                        int colonIdx = nextPattern.indexOf(':');
174                        Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern);
175                        String resourceType = nextPattern.substring(0, colonIdx);
176                        String searchParamName = nextPattern.substring(colonIdx + 1);
177                        Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern);
178                        Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern);
179                        if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) {
180                                continue;
181                        }
182                        if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) {
183                                continue;
184                        }
185                        return true;
186                }
187
188                return false;
189        }
190
191        public static ReadOnlySearchParamCache fromRuntimeSearchParamCache(
192                        RuntimeSearchParamCache theRuntimeSearchParamCache) {
193                return new ReadOnlySearchParamCache(theRuntimeSearchParamCache);
194        }
195}