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}