View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   import ca.uhn.fhir.context.FhirContext;
4   import ca.uhn.fhir.model.api.IQueryParameterAnd;
5   import ca.uhn.fhir.model.api.IQueryParameterOr;
6   import ca.uhn.fhir.model.api.IQueryParameterType;
7   import ca.uhn.fhir.model.api.Include;
8   import ca.uhn.fhir.rest.api.*;
9   import ca.uhn.fhir.rest.param.DateParam;
10  import ca.uhn.fhir.rest.param.DateRangeParam;
11  import ca.uhn.fhir.util.ObjectUtil;
12  import ca.uhn.fhir.util.UrlUtil;
13  import org.apache.commons.lang3.StringUtils;
14  import org.apache.commons.lang3.Validate;
15  import org.apache.commons.lang3.builder.ToStringBuilder;
16  import org.apache.commons.lang3.builder.ToStringStyle;
17  
18  import java.util.*;
19  
20  import static org.apache.commons.lang3.StringUtils.isNotBlank;
21  
22  /*
23   * #%L
24   * HAPI FHIR JPA Server
25   * %%
26   * Copyright (C) 2014 - 2018 University Health Network
27   * %%
28   * Licensed under the Apache License, Version 2.0 (the "License");
29   * you may not use this file except in compliance with the License.
30   * You may obtain a copy of the License at
31   * 
32   * http://www.apache.org/licenses/LICENSE-2.0
33   * 
34   * Unless required by applicable law or agreed to in writing, software
35   * distributed under the License is distributed on an "AS IS" BASIS,
36   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
37   * See the License for the specific language governing permissions and
38   * limitations under the License.
39   * #L%
40   */
41  
42  public class SearchParameterMap extends LinkedHashMap<String, List<List<? extends IQueryParameterType>>> {
43  
44  	private static final long serialVersionUID = 1L;
45  
46  	private Integer myCount;
47  	private EverythingModeEnum myEverythingMode = null;
48  	private Set<Include> myIncludes;
49  	private DateRangeParam myLastUpdated;
50  	private boolean myLoadSynchronous;
51  	private Integer myLoadSynchronousUpTo;
52  	private Set<Include> myRevIncludes;
53  	private SortSpec mySort;
54  	private SummaryEnum mySummaryMode;
55  	private SearchTotalModeEnum mySearchTotalMode;
56  
57  	/**
58  	 * Constructor
59  	 */
60  	public SearchParameterMap() {
61  		super();
62  	}
63  
64  	/**
65  	 * Constructor
66  	 */
67  	public SearchParameterMap(String theName, IQueryParameterType theParam) {
68  		add(theName, theParam);
69  	}
70  
71  	public SummaryEnum getSummaryMode() {
72  		return mySummaryMode;
73  	}
74  
75  	public void setSummaryMode(SummaryEnum theSummaryMode) {
76  		mySummaryMode = theSummaryMode;
77  	}
78  
79  	public SearchTotalModeEnum getSearchTotalMode() {
80  		return mySearchTotalMode;
81  	}
82  
83  	public void setSearchTotalMode(SearchTotalModeEnum theSearchTotalMode) {
84  		mySearchTotalMode = theSearchTotalMode;
85  	}
86  
87  	public SearchParameterMap add(String theName, DateParam theDateParam) {
88  		add(theName, (IQueryParameterOr<?>) theDateParam);
89  		return this;
90  	}
91  
92  	public void add(String theName, IQueryParameterAnd<?> theAnd) {
93  		if (theAnd == null) {
94  			return;
95  		}
96  		if (!containsKey(theName)) {
97  			put(theName, new ArrayList<>());
98  		}
99  
100 		for (IQueryParameterOr<?> next : theAnd.getValuesAsQueryTokens()) {
101 			if (next == null) {
102 				continue;
103 			}
104 			get(theName).add(next.getValuesAsQueryTokens());
105 		}
106 	}
107 
108 	public void add(String theName, IQueryParameterOr<?> theOr) {
109 		if (theOr == null) {
110 			return;
111 		}
112 		if (!containsKey(theName)) {
113 			put(theName, new ArrayList<>());
114 		}
115 
116 		get(theName).add(theOr.getValuesAsQueryTokens());
117 	}
118 
119 	public SearchParameterMap add(String theName, IQueryParameterType theParam) {
120 		assert !Constants.PARAM_LASTUPDATED.equals(theName); // this has it's own field in the map
121 
122 		if (theParam == null) {
123 			return this;
124 		}
125 		if (!containsKey(theName)) {
126 			put(theName, new ArrayList<>());
127 		}
128 		ArrayList<IQueryParameterType> list = new ArrayList<>();
129 		list.add(theParam);
130 		get(theName).add(list);
131 
132 		return this;
133 	}
134 
135 	public void addInclude(Include theInclude) {
136 		getIncludes().add(theInclude);
137 	}
138 
139 	private void addLastUpdateParam(StringBuilder b, DateParam date) {
140 		if (date != null && isNotBlank(date.getValueAsString())) {
141 			addUrlParamSeparator(b);
142 			b.append(Constants.PARAM_LASTUPDATED);
143 			b.append('=');
144 			b.append(date.getValueAsString());
145 		}
146 	}
147 
148 	public void addRevInclude(Include theInclude) {
149 		getRevIncludes().add(theInclude);
150 	}
151 
152 	private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> theList) {
153 		ArrayList<Include> list = new ArrayList<>(theList);
154 
155 		list.sort(new IncludeComparator());
156 		for (Include nextInclude : list) {
157 			addUrlParamSeparator(b);
158 			b.append(paramName);
159 			b.append('=');
160 			b.append(UrlUtil.escapeUrlParam(nextInclude.getParamType()));
161 			b.append(':');
162 			b.append(UrlUtil.escapeUrlParam(nextInclude.getParamName()));
163 			if (isNotBlank(nextInclude.getParamTargetType())) {
164 				b.append(':');
165 				b.append(nextInclude.getParamTargetType());
166 			}
167 		}
168 	}
169 
170 	private void addUrlParamSeparator(StringBuilder theB) {
171 		if (theB.length() == 0) {
172 			theB.append('?');
173 		} else {
174 			theB.append('&');
175 		}
176 	}
177 
178 	public Integer getCount() {
179 		return myCount;
180 	}
181 
182 	public void setCount(Integer theCount) {
183 		myCount = theCount;
184 	}
185 
186 	public EverythingModeEnum getEverythingMode() {
187 		return myEverythingMode;
188 	}
189 
190 	public void setEverythingMode(EverythingModeEnum theConsolidateMatches) {
191 		myEverythingMode = theConsolidateMatches;
192 	}
193 
194 	public Set<Include> getIncludes() {
195 		if (myIncludes == null) {
196 			myIncludes = new HashSet<>();
197 		}
198 		return myIncludes;
199 	}
200 
201 	public void setIncludes(Set<Include> theIncludes) {
202 		myIncludes = theIncludes;
203 	}
204 
205 	/**
206 	 * Returns null if there is no last updated value
207 	 */
208 	public DateRangeParam getLastUpdated() {
209 		if (myLastUpdated != null) {
210 			if (myLastUpdated.isEmpty()) {
211 				myLastUpdated = null;
212 			}
213 		}
214 		return myLastUpdated;
215 	}
216 
217 	public void setLastUpdated(DateRangeParam theLastUpdated) {
218 		myLastUpdated = theLastUpdated;
219 	}
220 
221 	/**
222 	 * If set, tells the server to load these results synchronously, and not to load
223 	 * more than X results
224 	 */
225 	public Integer getLoadSynchronousUpTo() {
226 		return myLoadSynchronousUpTo;
227 	}
228 
229 	/**
230 	 * If set, tells the server to load these results synchronously, and not to load
231 	 * more than X results. Note that setting this to a value will also set
232 	 * {@link #setLoadSynchronous(boolean)} to true
233 	 */
234 	public SearchParameterMap setLoadSynchronousUpTo(Integer theLoadSynchronousUpTo) {
235 		myLoadSynchronousUpTo = theLoadSynchronousUpTo;
236 		if (myLoadSynchronousUpTo != null) {
237 			setLoadSynchronous(true);
238 		}
239 		return this;
240 	}
241 
242 	public Set<Include> getRevIncludes() {
243 		if (myRevIncludes == null) {
244 			myRevIncludes = new HashSet<>();
245 		}
246 		return myRevIncludes;
247 	}
248 
249 	public void setRevIncludes(Set<Include> theRevIncludes) {
250 		myRevIncludes = theRevIncludes;
251 	}
252 
253 	public SortSpec getSort() {
254 		return mySort;
255 	}
256 
257 	public void setSort(SortSpec theSort) {
258 		mySort = theSort;
259 	}
260 
261 	/**
262 	 * This will only return true if all parameters have no modifier of any kind
263 	 */
264 	boolean isAllParametersHaveNoModifier() {
265 		for (List<List<? extends IQueryParameterType>> nextParamName : values()) {
266 			for (List<? extends IQueryParameterType> nextAnd : nextParamName) {
267 				for (IQueryParameterType nextOr : nextAnd) {
268 					if (isNotBlank(nextOr.getQueryParameterQualifier())) {
269 						return false;
270 					}
271 				}
272 			}
273 		}
274 		return true;
275 	}
276 
277 	/**
278 	 * If set, tells the server to load these results synchronously, and not to load
279 	 * more than X results
280 	 */
281 	public boolean isLoadSynchronous() {
282 		return myLoadSynchronous;
283 	}
284 
285 	/**
286 	 * If set, tells the server to load these results synchronously, and not to load
287 	 * more than X results
288 	 */
289 	public SearchParameterMap setLoadSynchronous(boolean theLoadSynchronous) {
290 		myLoadSynchronous = theLoadSynchronous;
291 		return this;
292 	}
293 
294 	/**
295 	 * This method creates a URL query string representation of the parameters in this
296 	 * object, excluding the part before the parameters, e.g.
297 	 * <p>
298 	 * <code>?name=smith&_sort=Patient:family</code>
299 	 * </p>
300 	 * <p>
301 	 * This method <b>excludes</b> the <code>_count</code> parameter,
302 	 * as it doesn't affect the substance of the results returned
303 	 * </p>
304 	 */
305 	public String toNormalizedQueryString(FhirContext theCtx) {
306 		StringBuilder b = new StringBuilder();
307 
308 		ArrayList<String> keys = new ArrayList<>(keySet());
309 		Collections.sort(keys);
310 		for (String nextKey : keys) {
311 
312 			List<List<? extends IQueryParameterType>> nextValuesAndsIn = get(nextKey);
313 			List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>();
314 
315 			for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) {
316 
317 				List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>();
318 				for (IQueryParameterType nextValueOrIn : nextValuesAndIn) {
319 					if (nextValueOrIn.getMissing() != null || isNotBlank(nextValueOrIn.getValueAsQueryToken(theCtx))) {
320 						nextValuesOrsOut.add(nextValueOrIn);
321 					}
322 				}
323 
324 				nextValuesOrsOut.sort(new QueryParameterTypeComparator(theCtx));
325 
326 				if (nextValuesOrsOut.size() > 0) {
327 					nextValuesAndsOut.add(nextValuesOrsOut);
328 				}
329 
330 			} // for AND
331 
332 			nextValuesAndsOut.sort(new QueryParameterOrComparator(theCtx));
333 
334 			for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) {
335 				addUrlParamSeparator(b);
336 				IQueryParameterType firstValue = nextValuesAnd.get(0);
337 				b.append(UrlUtil.escapeUrlParam(nextKey));
338 
339 				if (firstValue.getMissing() != null) {
340 					b.append(Constants.PARAMQUALIFIER_MISSING);
341 					b.append('=');
342 					if (firstValue.getMissing()) {
343 						b.append(Constants.PARAMQUALIFIER_MISSING_TRUE);
344 					} else {
345 						b.append(Constants.PARAMQUALIFIER_MISSING_FALSE);
346 					}
347 					continue;
348 				}
349 
350 				if (isNotBlank(firstValue.getQueryParameterQualifier())) {
351 					b.append(firstValue.getQueryParameterQualifier());
352 				}
353 
354 				b.append('=');
355 
356 				for (int i = 0; i < nextValuesAnd.size(); i++) {
357 					IQueryParameterType nextValueOr = nextValuesAnd.get(i);
358 					if (i > 0) {
359 						b.append(',');
360 					}
361 					String valueAsQueryToken = nextValueOr.getValueAsQueryToken(theCtx);
362 //					b.append(ParameterUtil.escapeAndUrlEncode(valueAsQueryToken));
363 					b.append(UrlUtil.escapeUrlParam(valueAsQueryToken));
364 				}
365 			}
366 
367 		} // for keys
368 
369 		SortSpec sort = getSort();
370 		boolean first = true;
371 		while (sort != null) {
372 
373 			if (isNotBlank(sort.getParamName())) {
374 				if (first) {
375 					addUrlParamSeparator(b);
376 					b.append(Constants.PARAM_SORT);
377 					b.append('=');
378 					first = false;
379 				} else {
380 					b.append(',');
381 				}
382 				if (sort.getOrder() == SortOrderEnum.DESC) {
383 					b.append('-');
384 				}
385 				b.append(sort.getParamName());
386 			}
387 
388 			Validate.isTrue(sort != sort.getChain()); // just in case, shouldn't happen
389 			sort = sort.getChain();
390 		}
391 
392 		addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes());
393 		addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes());
394 
395 		if (getLastUpdated() != null) {
396 			DateParam lb = getLastUpdated().getLowerBound();
397 			addLastUpdateParam(b, lb);
398 			DateParam ub = getLastUpdated().getUpperBound();
399 			addLastUpdateParam(b, ub);
400 		}
401 
402 		if (getCount() != null) {
403 			addUrlParamSeparator(b);
404 			b.append(Constants.PARAM_COUNT);
405 			b.append('=');
406 			b.append(getCount());
407 		}
408 
409 		// Summary
410 		if (getSummaryMode() != null) {
411 			addUrlParamSeparator(b);
412 			b.append(Constants.PARAM_SUMMARY);
413 			b.append('=');
414 			b.append(getSummaryMode().getCode());
415 		}
416 
417 		if (b.length() == 0) {
418 			b.append('?');
419 		}
420 
421 		return b.toString();
422 	}
423 
424 	@Override
425 	public String toString() {
426 		ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
427 		if (isEmpty() == false) {
428 			b.append("params", super.toString());
429 		}
430 		if (getIncludes().isEmpty() == false) {
431 			b.append("includes", getIncludes());
432 		}
433 		return b.toString();
434 	}
435 
436 	public enum EverythingModeEnum {
437 		/*
438 		 * Don't reorder! We rely on the ordinals
439 		 */
440 		ENCOUNTER_INSTANCE(false, true, true),
441 		ENCOUNTER_TYPE(false, true, false),
442 		PATIENT_INSTANCE(true, false, true),
443 		PATIENT_TYPE(true, false, false);
444 
445 		private final boolean myEncounter;
446 
447 		private final boolean myInstance;
448 
449 		private final boolean myPatient;
450 
451 		EverythingModeEnum(boolean thePatient, boolean theEncounter, boolean theInstance) {
452 			assert thePatient ^ theEncounter;
453 			myPatient = thePatient;
454 			myEncounter = theEncounter;
455 			myInstance = theInstance;
456 		}
457 
458 		public boolean isEncounter() {
459 			return myEncounter;
460 		}
461 
462 		public boolean isInstance() {
463 			return myInstance;
464 		}
465 
466 		public boolean isPatient() {
467 			return myPatient;
468 		}
469 	}
470 
471 	public class IncludeComparator implements Comparator<Include> {
472 
473 		@Override
474 		public int compare(Include theO1, Include theO2) {
475 			int retVal = StringUtils.compare(theO1.getParamType(), theO2.getParamType());
476 			if (retVal == 0) {
477 				retVal = StringUtils.compare(theO1.getParamName(), theO2.getParamName());
478 			}
479 			if (retVal == 0) {
480 				retVal = StringUtils.compare(theO1.getParamTargetType(), theO2.getParamTargetType());
481 			}
482 			return retVal;
483 		}
484 
485 	}
486 
487 	public class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> {
488 		private final FhirContext myCtx;
489 
490 		QueryParameterOrComparator(FhirContext theCtx) {
491 			myCtx = theCtx;
492 		}
493 
494 		@Override
495 		public int compare(List<IQueryParameterType> theO1, List<IQueryParameterType> theO2) {
496 			// These lists will never be empty
497 			return SearchParameterMap.compare(myCtx, theO1.get(0), theO2.get(0));
498 		}
499 
500 	}
501 
502 	public class QueryParameterTypeComparator implements Comparator<IQueryParameterType> {
503 
504 		private final FhirContext myCtx;
505 
506 		QueryParameterTypeComparator(FhirContext theCtx) {
507 			myCtx = theCtx;
508 		}
509 
510 		@Override
511 		public int compare(IQueryParameterType theO1, IQueryParameterType theO2) {
512 			return SearchParameterMap.compare(myCtx, theO1, theO2);
513 		}
514 
515 	}
516 
517 	private static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) {
518 		int retVal;
519 		if (theO1.getMissing() == null && theO2.getMissing() == null) {
520 			retVal = 0;
521 		} else if (theO1.getMissing() == null) {
522 			retVal = -1;
523 		} else if (theO2.getMissing() == null) {
524 			retVal = 1;
525 		} else if (ObjectUtil.equals(theO1.getMissing(), theO2.getMissing())) {
526 			retVal = 0;
527 		} else {
528 			if (theO1.getMissing()) {
529 				retVal = 1;
530 			} else {
531 				retVal = -1;
532 			}
533 		}
534 
535 		if (retVal == 0) {
536 			String q1 = theO1.getQueryParameterQualifier();
537 			String q2 = theO2.getQueryParameterQualifier();
538 			retVal = StringUtils.compare(q1, q2);
539 		}
540 
541 		if (retVal == 0) {
542 			String v1 = theO1.getValueAsQueryToken(theCtx);
543 			String v2 = theO2.getValueAsQueryToken(theCtx);
544 			retVal = StringUtils.compare(v1, v2);
545 		}
546 		return retVal;
547 	}
548 
549 }