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