001/*- 002 * #%L 003 * Smile CDR - CDR 004 * %% 005 * Copyright (C) 2016 - 2026 Smile CDR, Inc. 006 * %% 007 * All rights reserved. 008 * #L% 009 */ 010package ca.cdr.api.fhirgw.model; 011 012import ca.uhn.fhir.model.api.Include; 013import ca.uhn.fhir.rest.api.SearchStyleEnum; 014import ca.uhn.fhir.rest.api.SearchTotalModeEnum; 015import ca.uhn.fhir.rest.api.SortOrderEnum; 016import ca.uhn.fhir.rest.api.SortSpec; 017import ca.uhn.fhir.rest.param.DateRangeParam; 018import ca.uhn.fhir.rest.param.UriAndListParam; 019import ca.uhn.fhir.rest.param.UriOrListParam; 020import ca.uhn.fhir.rest.param.UriParam; 021import ca.uhn.fhir.util.UrlUtil; 022import com.google.common.collect.ArrayListMultimap; 023import com.google.common.collect.ListMultimap; 024import com.google.common.collect.Multimap; 025import com.google.common.collect.Multimaps; 026import jakarta.annotation.Nonnull; 027import jakarta.annotation.Nullable; 028import org.apache.commons.lang3.Validate; 029import org.springframework.web.util.UriComponentsBuilder; 030 031import java.util.Collections; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036 037import static org.apache.commons.lang3.ObjectUtils.getIfNull; 038 039/** 040 * This class represents a FHIR Gateway Search request 041 */ 042public class SearchRequest extends BaseRequest<SearchRequest> { 043 044 private ListMultimap<String, String> myParameters = EMPTY_STRING_MULTIMAP; 045 private Set<Include> myIncludes = Collections.emptySet(); 046 private Set<Include> myRevIncludes = Collections.emptySet(); 047 private SortSpec mySort; 048 private Integer myCount; 049 private SearchTotalModeEnum myTotalMode; 050 private Integer myOffset; 051 private DateRangeParam myLastUpdated; 052 private UriAndListParam myProfile; 053 054 @Nullable 055 private SearchStyleEnum mySearchStyle; 056 057 /** 058 * Constructor 059 */ 060 public SearchRequest() { 061 // nothing 062 } 063 064 /** 065 * Copy constructor 066 */ 067 public SearchRequest(SearchRequest theSearchRequest) { 068 super(theSearchRequest); 069 070 if (!theSearchRequest.getParameters().isEmpty()) { 071 myParameters = ArrayListMultimap.create(); 072 myParameters.putAll(theSearchRequest.getParameters()); 073 } 074 075 if (!theSearchRequest.getIncludes().isEmpty()) { 076 myIncludes = new HashSet<>(); 077 for (Include next : theSearchRequest.getIncludes()) { 078 myIncludes.add(next.toLocked()); 079 } 080 } 081 082 if (!theSearchRequest.getRevIncludes().isEmpty()) { 083 myRevIncludes = new HashSet<>(); 084 for (Include next : theSearchRequest.getRevIncludes()) { 085 myRevIncludes.add(next.toLocked()); 086 } 087 } 088 089 if (theSearchRequest.getSort() != null) { 090 mySort = clone(theSearchRequest.getSort()); 091 } 092 093 if (theSearchRequest.getLastUpdated() != null) { 094 myLastUpdated = new DateRangeParam( 095 theSearchRequest.getLastUpdated().getLowerBound() != null 096 ? theSearchRequest.getLastUpdated().getLowerBound().getValueAsString() 097 : null, 098 theSearchRequest.getLastUpdated().getUpperBound() != null 099 ? theSearchRequest.getLastUpdated().getUpperBound().getValueAsString() 100 : null); 101 } 102 103 if (theSearchRequest.getProfile() != null) { 104 myProfile = new UriAndListParam(); 105 for (UriOrListParam uriOrListParam : theSearchRequest.getProfile().getValuesAsQueryTokens()) { 106 myProfile.addAnd(uriOrListParam); 107 } 108 } 109 110 myCount = theSearchRequest.getCount(); 111 myOffset = theSearchRequest.getOffset(); 112 myTotalMode = theSearchRequest.getTotalMode(); 113 mySearchStyle = theSearchRequest.getSearchStyle(); 114 } 115 116 private static SortSpec clone(SortSpec theSort) { 117 if (theSort == null) { 118 return null; 119 } 120 return new SortSpec(theSort.getParamName(), theSort.getOrder(), clone(theSort.getChain())); 121 } 122 123 /** 124 * Specifies the search form (GET vs POST) to use for this search. If set to 125 * <code>null</code>, the default will be used. 126 */ 127 @Nullable 128 public SearchStyleEnum getSearchStyle() { 129 return mySearchStyle; 130 } 131 132 /** 133 * Specifies the search form (GET vs POST) to use for this search. If set to 134 * <code>null</code>, the default will be used. 135 */ 136 public void setSearchStyle(@Nullable SearchStyleEnum theSearchStyle) { 137 mySearchStyle = theSearchStyle; 138 } 139 140 public Multimap<String, String> getParameters() { 141 return Multimaps.unmodifiableMultimap(myParameters); 142 } 143 144 public SearchRequest setParameters(Map<String, List<String>> theParameters) { 145 Validate.notNull(theParameters, "theParameters must not be null"); 146 myParameters = ArrayListMultimap.create(); 147 for (Map.Entry<String, List<String>> next : theParameters.entrySet()) { 148 myParameters.putAll(next.getKey(), next.getValue()); 149 } 150 return this; 151 } 152 153 /** 154 * Add a new search parameter 155 */ 156 public void addParameter(String theName, String theValue) { 157 Validate.notBlank(theName, "theName must not be empty"); 158 Validate.notBlank(theValue, "theValue must not be empty"); 159 if (myParameters.isEmpty()) { 160 myParameters = ArrayListMultimap.create(); 161 } 162 myParameters.put(theName, theValue); 163 } 164 165 /** 166 * Adds OR parameters to the list of search parameters as a key value 167 * pair where the value is a comma-delimited list i.e. _id=123,345 168 * 169 * @param theName The parameter name 170 * @param theValues the values for the parameter 171 */ 172 public void addOrListParameter(String theName, List<String> theValues) { 173 Validate.notBlank(theName, "theName must not be empty"); 174 Validate.notEmpty(theValues, "theValues must not be empty"); 175 String values = String.join(",", theValues); 176 if (myParameters.isEmpty()) { 177 myParameters = ArrayListMultimap.create(); 178 } 179 myParameters.put(theName, values); 180 } 181 182 /** 183 * Remove all parameters with the given name 184 * 185 * @return Returns any parameter values that were removed 186 */ 187 public List<String> removeParameters(String theName) { 188 if (!myParameters.isEmpty()) { 189 return Collections.unmodifiableList(myParameters.removeAll(theName)); 190 } else { 191 return Collections.emptyList(); 192 } 193 } 194 195 /** 196 * Remove the parameter with the given name and value, if found. Any other parameters with the same name are kept. 197 * 198 * @param theName The parameter name 199 * @param theValue The parameter value 200 * @return Returns <code>true</code> if a parameter value was removed 201 */ 202 public boolean removeParameter(String theName, String theValue) { 203 return myParameters.remove(theName, theValue); 204 } 205 206 /** 207 * Returns the first parameter value for a given parameter name (returns <code>null</code> if no parameters match the given name) 208 * 209 * @param theName The parameter name 210 * @return The value, or <code>null</code> 211 */ 212 @Nullable 213 public String getParameter(String theName) { 214 List<String> values = getParameters(theName); 215 if (!values.isEmpty()) { 216 return values.get(0); 217 } 218 return null; 219 } 220 221 /** 222 * Returns a List of parameter values for the given name. Will not return <code>null</code>. 223 * 224 * @param theName The parameter name 225 * @return A List of values, or an empty List 226 */ 227 @Nonnull 228 public List<String> getParameters(String theName) { 229 List<String> values = myParameters.get(theName); 230 values = getIfNull(values, Collections.emptyList()); 231 return Collections.unmodifiableList(values); 232 } 233 234 public Set<Include> getIncludes() { 235 return myIncludes; 236 } 237 238 public SearchRequest setIncludes(Set<Include> theIncludes) { 239 Validate.notNull(theIncludes, "theIncludes must not be null"); 240 myIncludes = theIncludes; 241 return this; 242 } 243 244 public Set<Include> getRevIncludes() { 245 return myRevIncludes; 246 } 247 248 public SearchRequest setRevIncludes(Set<Include> theRevIncludes) { 249 Validate.notNull(theRevIncludes, "theRevIncludes must not be null"); 250 myRevIncludes = theRevIncludes; 251 return this; 252 } 253 254 public SortSpec getSort() { 255 return mySort; 256 } 257 258 public SearchRequest setSort(SortSpec theSort) { 259 mySort = theSort; 260 return this; 261 } 262 263 public Integer getCount() { 264 return myCount; 265 } 266 267 public SearchRequest setCount(Integer theCount) { 268 myCount = theCount; 269 return this; 270 } 271 272 public Integer getOffset() { 273 return myOffset; 274 } 275 276 public SearchRequest setOffset(Integer theOffset) { 277 myOffset = theOffset; 278 return this; 279 } 280 281 public DateRangeParam getLastUpdated() { 282 return myLastUpdated; 283 } 284 285 public SearchRequest setLastUpdated(DateRangeParam theLastUpdated) { 286 myLastUpdated = theLastUpdated; 287 return this; 288 } 289 290 public UriAndListParam getProfile() { 291 return myProfile; 292 } 293 294 public SearchRequest setProfile(UriAndListParam theProfile) { 295 myProfile = theProfile; 296 return this; 297 } 298 299 public SearchTotalModeEnum getTotalMode() { 300 return myTotalMode; 301 } 302 303 public SearchRequest setTotalMode(SearchTotalModeEnum theTotalMode) { 304 myTotalMode = theTotalMode; 305 return this; 306 } 307 308 /** 309 * Serializes the structured search parameters into a URL query string. 310 * Returns an empty string if there are no parameters, or a string starting with "?" 311 * containing all raw and structured parameters. 312 */ 313 public String toQueryString() { 314 UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); 315 316 // Raw parameters 317 for (Map.Entry<String, String> entry : myParameters.entries()) { 318 builder.queryParam(UrlUtil.escapeUrlParam(entry.getKey()), UrlUtil.escapeUrlParam(entry.getValue())); 319 } 320 321 for (Include include : myIncludes) { 322 String paramName = include.isRecurse() ? "_include:iterate" : "_include"; 323 builder.queryParam(UrlUtil.escapeUrlParam(paramName), UrlUtil.escapeUrlParam(include.getValue())); 324 } 325 326 for (Include revInclude : myRevIncludes) { 327 String paramName = revInclude.isRecurse() ? "_revinclude:iterate" : "_revinclude"; 328 builder.queryParam(UrlUtil.escapeUrlParam(paramName), UrlUtil.escapeUrlParam(revInclude.getValue())); 329 } 330 331 if (myCount != null) { 332 builder.queryParam("_count", myCount); 333 } 334 335 if (myOffset != null) { 336 builder.queryParam("_offset", myOffset); 337 } 338 339 if (mySort != null) { 340 builder.queryParam(UrlUtil.escapeUrlParam("_sort"), UrlUtil.escapeUrlParam(serializeSort(mySort))); 341 } 342 343 if (myLastUpdated != null) { 344 if (myLastUpdated.getLowerBound() != null) { 345 builder.queryParam( 346 UrlUtil.escapeUrlParam("_lastUpdated"), 347 UrlUtil.escapeUrlParam( 348 myLastUpdated.getLowerBound().getPrefix().getValue() 349 + myLastUpdated.getLowerBound().getValueAsString())); 350 } 351 if (myLastUpdated.getUpperBound() != null) { 352 builder.queryParam( 353 UrlUtil.escapeUrlParam("_lastUpdated"), 354 UrlUtil.escapeUrlParam( 355 myLastUpdated.getUpperBound().getPrefix().getValue() 356 + myLastUpdated.getUpperBound().getValueAsString())); 357 } 358 } 359 360 if (myTotalMode != null) { 361 builder.queryParam("_total", myTotalMode.getCode()); 362 } 363 364 if (myProfile != null) { 365 for (UriOrListParam orList : myProfile.getValuesAsQueryTokens()) { 366 StringBuilder profileValue = new StringBuilder(); 367 boolean firstOr = true; 368 for (UriParam uriParam : orList.getValuesAsQueryTokens()) { 369 if (!firstOr) { 370 profileValue.append(UrlUtil.escapeUrlParam(",")); 371 } 372 profileValue.append(UrlUtil.escapeUrlParam(uriParam.getValue())); 373 firstOr = false; 374 } 375 builder.queryParam(UrlUtil.escapeUrlParam("_profile"), profileValue.toString()); 376 } 377 } 378 379 return builder.build().toUriString(); 380 } 381 382 private static String serializeSort(SortSpec theSort) { 383 StringBuilder sb = new StringBuilder(); 384 SortSpec current = theSort; 385 boolean first = true; 386 while (current != null) { 387 if (!first) { 388 sb.append(","); 389 } 390 if (current.getOrder() == SortOrderEnum.DESC) { 391 sb.append("-"); 392 } 393 sb.append(current.getParamName()); 394 current = current.getChain(); 395 first = false; 396 } 397 return sb.toString(); 398 } 399}