001/*
002 * #%L
003 * HAPI FHIR - Core Library
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.context;
021
022import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
023import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
024import jakarta.annotation.Nonnull;
025import jakarta.annotation.Nullable;
026import org.apache.commons.lang3.builder.EqualsBuilder;
027import org.apache.commons.lang3.builder.HashCodeBuilder;
028import org.apache.commons.lang3.builder.ToStringBuilder;
029import org.apache.commons.lang3.builder.ToStringStyle;
030import org.hl7.fhir.instance.model.api.IBaseExtension;
031import org.hl7.fhir.instance.model.api.IIdType;
032
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.StringTokenizer;
042
043import static org.apache.commons.lang3.StringUtils.isNotBlank;
044import static org.apache.commons.lang3.StringUtils.trim;
045
046public class RuntimeSearchParam {
047        private final IIdType myId;
048        private final Set<String> myBase;
049        private final String myDescription;
050        private final String myName;
051        private final RestSearchParameterTypeEnum myParamType;
052        private final String myPath;
053        private final Set<String> myTargets;
054        private final Set<String> myProvidesMembershipInCompartments;
055        private final RuntimeSearchParamStatusEnum myStatus;
056        private final String myUri;
057        private final Map<String, List<IBaseExtension<?, ?>>> myExtensions = new HashMap<>();
058        private final Map<String, String> myUpliftRefchains = new HashMap<>();
059        private final ComboSearchParamType myComboSearchParamType;
060        private final List<Component> myComponents;
061        private final IIdType myIdUnqualifiedVersionless;
062        private IPhoneticEncoder myPhoneticEncoder;
063
064        /**
065         * Constructor
066         */
067        public RuntimeSearchParam(
068                        IIdType theId,
069                        String theUri,
070                        String theName,
071                        String theDescription,
072                        String thePath,
073                        RestSearchParameterTypeEnum theParamType,
074                        Set<String> theProvidesMembershipInCompartments,
075                        Set<String> theTargets,
076                        RuntimeSearchParamStatusEnum theStatus,
077                        Collection<String> theBase) {
078                this(
079                                theId,
080                                theUri,
081                                theName,
082                                theDescription,
083                                thePath,
084                                theParamType,
085                                theProvidesMembershipInCompartments,
086                                theTargets,
087                                theStatus,
088                                null,
089                                Collections.emptyList(),
090                                theBase);
091        }
092
093        /**
094         * Copy constructor
095         */
096        public RuntimeSearchParam(RuntimeSearchParam theSp) {
097                this(
098                                theSp.getId(),
099                                theSp.getUri(),
100                                theSp.getName(),
101                                theSp.getDescription(),
102                                theSp.getPath(),
103                                theSp.getParamType(),
104                                theSp.getProvidesMembershipInCompartments(),
105                                theSp.getTargets(),
106                                theSp.getStatus(),
107                                theSp.getComboSearchParamType(),
108                                theSp.getComponents(),
109                                theSp.getBase());
110        }
111
112        /**
113         * Constructor
114         */
115        public RuntimeSearchParam(
116                        IIdType theId,
117                        String theUri,
118                        String theName,
119                        String theDescription,
120                        String thePath,
121                        RestSearchParameterTypeEnum theParamType,
122                        Set<String> theProvidesMembershipInCompartments,
123                        Set<String> theTargets,
124                        RuntimeSearchParamStatusEnum theStatus,
125                        ComboSearchParamType theComboSearchParamType,
126                        List<Component> theComponents,
127                        Collection<String> theBase) {
128                super();
129
130                myId = theId;
131                myIdUnqualifiedVersionless = theId != null ? theId.toUnqualifiedVersionless() : null;
132                myUri = theUri;
133                myName = theName;
134                myDescription = theDescription;
135                myPath = thePath;
136                myParamType = theParamType;
137                myStatus = theStatus;
138                if (theProvidesMembershipInCompartments != null && !theProvidesMembershipInCompartments.isEmpty()) {
139                        myProvidesMembershipInCompartments = Collections.unmodifiableSet(theProvidesMembershipInCompartments);
140                } else {
141                        myProvidesMembershipInCompartments = null;
142                }
143                if (theTargets != null && theTargets.isEmpty() == false) {
144                        myTargets = Collections.unmodifiableSet(theTargets);
145                } else {
146                        myTargets = Collections.emptySet();
147                }
148
149                if (theBase == null || theBase.isEmpty()) {
150                        HashSet<String> base = new HashSet<>();
151                        if (isNotBlank(thePath)) {
152                                int indexOf = thePath.indexOf('.');
153                                if (indexOf != -1) {
154                                        base.add(trim(thePath.substring(0, indexOf)));
155                                }
156                        }
157                        myBase = Collections.unmodifiableSet(base);
158                } else {
159                        myBase = Collections.unmodifiableSet(new HashSet<>(theBase));
160                }
161                myComboSearchParamType = theComboSearchParamType;
162                if (theComponents != null) {
163                        myComponents = Collections.unmodifiableList(theComponents);
164                } else {
165                        myComponents = Collections.emptyList();
166                }
167        }
168
169        public List<Component> getComponents() {
170                return myComponents;
171        }
172
173        /**
174         * Returns <code>null</code> if this is not a combo search param type
175         */
176        @Nullable
177        public ComboSearchParamType getComboSearchParamType() {
178                return myComboSearchParamType;
179        }
180
181        /**
182         * Retrieve user data - This can be used to store any application-specific data
183         */
184        @Nonnull
185        public List<IBaseExtension<?, ?>> getExtensions(String theKey) {
186                List<IBaseExtension<?, ?>> retVal = myExtensions.get(theKey);
187                if (retVal != null) {
188                        retVal = Collections.unmodifiableList(retVal);
189                } else {
190                        retVal = Collections.emptyList();
191                }
192                return retVal;
193        }
194
195        /**
196         * Sets user data - This can be used to store any application-specific data
197         */
198        public RuntimeSearchParam addExtension(String theKey, IBaseExtension<?, ?> theValue) {
199                List<IBaseExtension<?, ?>> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>());
200                valuesList.add(theValue);
201                return this;
202        }
203
204        @Override
205        public String toString() {
206                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
207                                .append("base", myBase)
208                                .append("name", myName)
209                                .append("path", myPath)
210                                .append("id", myId)
211                                .append("uri", myUri)
212                                .toString();
213        }
214
215        public IIdType getId() {
216                return myId;
217        }
218
219        public IIdType getIdUnqualifiedVersionless() {
220                return myIdUnqualifiedVersionless;
221        }
222
223        public String getUri() {
224                return myUri;
225        }
226
227        @Override
228        public boolean equals(Object theO) {
229                if (this == theO) return true;
230
231                if (theO == null || getClass() != theO.getClass()) return false;
232
233                RuntimeSearchParam that = (RuntimeSearchParam) theO;
234
235                return new EqualsBuilder()
236                                .append(getId(), that.getId())
237                                .append(getName(), that.getName())
238                                .append(getPath(), that.getPath())
239                                .append(getUri(), that.getUri())
240                                .isEquals();
241        }
242
243        @Override
244        public int hashCode() {
245                return new HashCodeBuilder(17, 37)
246                                .append(getId())
247                                .append(getName())
248                                .append(getPath())
249                                .append(getUri())
250                                .toHashCode();
251        }
252
253        public Set<String> getBase() {
254                return myBase;
255        }
256
257        @Nonnull
258        public Set<String> getTargets() {
259                return myTargets;
260        }
261
262        public boolean hasTargets() {
263                return !myTargets.isEmpty();
264        }
265
266        public RuntimeSearchParamStatusEnum getStatus() {
267                return myStatus;
268        }
269
270        public String getDescription() {
271                return myDescription;
272        }
273
274        public String getName() {
275                return myName;
276        }
277
278        public RestSearchParameterTypeEnum getParamType() {
279                return myParamType;
280        }
281
282        public String getPath() {
283                return myPath;
284        }
285
286        public List<String> getPathsSplit() {
287                return getPathsSplitForResourceType(null);
288        }
289
290        /**
291         * Can return null
292         */
293        public Set<String> getProvidesMembershipInCompartments() {
294                return myProvidesMembershipInCompartments;
295        }
296
297        public RuntimeSearchParam setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
298                myPhoneticEncoder = thePhoneticEncoder;
299                return this;
300        }
301
302        public String encode(String theString) {
303                if (myPhoneticEncoder == null || theString == null) {
304                        return theString;
305                }
306                return myPhoneticEncoder.encode(theString);
307        }
308
309        public List<String> getPathsSplitForResourceType(@Nullable String theResourceName) {
310                String path = getPath();
311                if (path.indexOf('|') == -1) {
312                        if (theResourceName != null && !pathMatchesResourceType(theResourceName, path)) {
313                                return Collections.emptyList();
314                        }
315                        return Collections.singletonList(path);
316                }
317
318                List<String> retVal = new ArrayList<>();
319                StringTokenizer tok = new StringTokenizer(path, "|");
320                while (tok.hasMoreElements()) {
321                        String nextPath = tok.nextToken().trim();
322                        if (theResourceName != null && !pathMatchesResourceType(theResourceName, nextPath)) {
323                                continue;
324                        }
325                        retVal.add(nextPath.trim());
326                }
327                return retVal;
328        }
329
330        public void addUpliftRefchain(@Nonnull String theCode, @Nonnull String theElementName) {
331                myUpliftRefchains.put(theCode, theElementName);
332        }
333
334        /**
335         * Does this search parameter have an uplift refchain definition for the given code?
336         * See the HAPI FHIR documentation for a description of how uplift refchains work.
337         *
338         * @since 6.6.0
339         */
340        public boolean hasUpliftRefchain(String theCode) {
341                return myUpliftRefchains.containsKey(theCode);
342        }
343
344        /**
345         * Returns a set of all codes associated with uplift refchains for this search parameter.
346         * See the HAPI FHIR documentation for a description of how uplift refchains work.
347         *
348         * @since 6.6.0
349         */
350        public Set<String> getUpliftRefchainCodes() {
351                return Collections.unmodifiableSet(myUpliftRefchains.keySet());
352        }
353
354        /**
355         * Does this search parameter have any uplift refchain definitions?
356         * See the HAPI FHIR documentation for a description of how uplift refchains work.
357         *
358         * @since 6.6.0
359         */
360        public boolean hasUpliftRefchains() {
361                return !myUpliftRefchains.isEmpty();
362        }
363
364        public enum RuntimeSearchParamStatusEnum {
365                ACTIVE,
366                DRAFT,
367                RETIRED,
368                UNKNOWN
369        }
370
371        /**
372         * This method tests whether a given FHIRPath expression <i>could</i>
373         * possibly apply to the given resource type.
374         *
375         * @param theResourceName
376         * @param thePath
377         * @return
378         */
379        static boolean pathMatchesResourceType(String theResourceName, String thePath) {
380                for (int i = 0; i < thePath.length() - 1; i++) {
381                        char nextChar = thePath.charAt(i);
382                        if (Character.isLowerCase(nextChar)) {
383                                return true;
384                        }
385                        if (Character.isLetter(nextChar)) {
386                                if (fhirPathExpressionStartsWith(theResourceName, thePath, i)) {
387                                        return true;
388                                }
389                                if (fhirPathExpressionStartsWith("Resource", thePath, i)) {
390                                        return true;
391                                }
392                                if (fhirPathExpressionStartsWith("DomainResource", thePath, i)) {
393                                        return true;
394                                }
395                                return false;
396                        }
397                }
398
399                return false;
400        }
401
402        private static boolean fhirPathExpressionStartsWith(String theResourceName, String thePath, int theStartingIndex) {
403                if (thePath.startsWith(theResourceName, theStartingIndex) && thePath.length() > theResourceName.length()) {
404                        for (int i = theResourceName.length() + theStartingIndex; i < thePath.length(); i++) {
405                                char nextChar = thePath.charAt(i);
406                                if (nextChar == '.') {
407                                        return true;
408                                } else if (nextChar != ' ') {
409                                        return false;
410                                }
411                        }
412                }
413                return false;
414        }
415
416        public static class Component {
417                private final String myExpression;
418                private final String myReference;
419
420                /**
421                 * Constructor
422                 */
423                public Component(String theExpression, String theReference) {
424                        myExpression = theExpression;
425                        myReference = theReference;
426                }
427
428                @Override
429                public String toString() {
430                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
431                                        .append("expression", myExpression)
432                                        .append("reference", myReference)
433                                        .toString();
434                }
435
436                public String getExpression() {
437                        return myExpression;
438                }
439
440                public String getReference() {
441                        return myReference;
442                }
443        }
444}