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