![](/hapi-fhir/images/logos/raccoon-forwards.png)
001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 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.jpa.searchparam; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.model.util.JpaConstants; 028import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 029import ca.uhn.fhir.model.api.IQueryParameterAnd; 030import ca.uhn.fhir.model.api.IQueryParameterType; 031import ca.uhn.fhir.model.api.Include; 032import ca.uhn.fhir.rest.api.Constants; 033import ca.uhn.fhir.rest.api.QualifiedParamList; 034import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 035import ca.uhn.fhir.rest.api.SearchTotalModeEnum; 036import ca.uhn.fhir.rest.param.DateRangeParam; 037import ca.uhn.fhir.rest.param.ParameterUtil; 038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 039import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 040import ca.uhn.fhir.util.ReflectionUtil; 041import ca.uhn.fhir.util.UrlUtil; 042import com.google.common.collect.ArrayListMultimap; 043import org.apache.http.NameValuePair; 044import org.springframework.beans.factory.annotation.Autowired; 045 046import java.util.List; 047 048import static org.apache.commons.lang3.StringUtils.isBlank; 049import static org.apache.commons.lang3.StringUtils.isNotBlank; 050 051public class MatchUrlService { 052 053 @Autowired 054 private FhirContext myFhirContext; 055 056 @Autowired 057 private ISearchParamRegistry mySearchParamRegistry; 058 059 public SearchParameterMap translateMatchUrl( 060 String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, Flag... theFlags) { 061 SearchParameterMap paramMap = new SearchParameterMap(); 062 List<NameValuePair> parameters = UrlUtil.translateMatchUrl(theMatchUrl); 063 064 ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create(); 065 for (NameValuePair next : parameters) { 066 if (isBlank(next.getValue())) { 067 continue; 068 } 069 070 String paramName = next.getName(); 071 String qualifier = null; 072 for (int i = 0; i < paramName.length(); i++) { 073 switch (paramName.charAt(i)) { 074 case '.': 075 case ':': 076 qualifier = paramName.substring(i); 077 paramName = paramName.substring(0, i); 078 i = Integer.MAX_VALUE - 1; 079 break; 080 } 081 } 082 083 QualifiedParamList paramList = 084 QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue()); 085 nameToParamLists.put(paramName, paramList); 086 } 087 088 for (String nextParamName : nameToParamLists.keySet()) { 089 List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName); 090 091 if (theFlags != null) { 092 for (Flag next : theFlags) { 093 next.process(nextParamName, paramList, paramMap); 094 } 095 } 096 097 if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { 098 if (paramList != null && paramList.size() > 0) { 099 if (paramList.size() > 2) { 100 throw new InvalidRequestException(Msg.code(484) + "Failed to parse match URL[" + theMatchUrl 101 + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED 102 + " parameter repetitions"); 103 } else { 104 DateRangeParam p1 = new DateRangeParam(); 105 p1.setValuesAsQueryTokens(myFhirContext, nextParamName, paramList); 106 paramMap.setLastUpdated(p1); 107 } 108 } 109 } else if (Constants.PARAM_HAS.equals(nextParamName)) { 110 IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams( 111 myFhirContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList); 112 paramMap.add(nextParamName, param); 113 } else if (Constants.PARAM_COUNT.equals(nextParamName)) { 114 if (paramList != null 115 && paramList.size() > 0 116 && paramList.get(0).size() > 0) { 117 String intString = paramList.get(0).get(0); 118 try { 119 paramMap.setCount(Integer.parseInt(intString)); 120 } catch (NumberFormatException e) { 121 throw new InvalidRequestException( 122 Msg.code(485) + "Invalid " + Constants.PARAM_COUNT + " value: " + intString); 123 } 124 } 125 } else if (Constants.PARAM_SEARCH_TOTAL_MODE.equals(nextParamName)) { 126 if (paramList != null 127 && !paramList.isEmpty() 128 && !paramList.get(0).isEmpty()) { 129 String totalModeEnumStr = paramList.get(0).get(0); 130 SearchTotalModeEnum searchTotalMode = SearchTotalModeEnum.fromCode(totalModeEnumStr); 131 if (searchTotalMode == null) { 132 // We had an oops here supporting the UPPER CASE enum instead of the FHIR code for _total. 133 // Keep supporting it in case someone is using it. 134 try { 135 paramMap.setSearchTotalMode(SearchTotalModeEnum.valueOf(totalModeEnumStr)); 136 } catch (IllegalArgumentException e) { 137 throw new InvalidRequestException(Msg.code(2078) + "Invalid " 138 + Constants.PARAM_SEARCH_TOTAL_MODE + " value: " + totalModeEnumStr); 139 } 140 } 141 paramMap.setSearchTotalMode(searchTotalMode); 142 } 143 } else if (Constants.PARAM_OFFSET.equals(nextParamName)) { 144 if (paramList != null 145 && paramList.size() > 0 146 && paramList.get(0).size() > 0) { 147 String intString = paramList.get(0).get(0); 148 try { 149 paramMap.setOffset(Integer.parseInt(intString)); 150 } catch (NumberFormatException e) { 151 throw new InvalidRequestException( 152 Msg.code(486) + "Invalid " + Constants.PARAM_OFFSET + " value: " + intString); 153 } 154 } 155 } else if (ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(nextParamName)) { 156 if (isNotBlank(paramList.get(0).getQualifier()) 157 && paramList.get(0).getQualifier().startsWith(".")) { 158 throw new InvalidRequestException(Msg.code(487) + "Invalid parameter chain: " + nextParamName 159 + paramList.get(0).getQualifier()); 160 } 161 IQueryParameterAnd<?> type = newInstanceAnd(nextParamName); 162 type.setValuesAsQueryTokens(myFhirContext, nextParamName, (paramList)); 163 paramMap.add(nextParamName, type); 164 } else if (Constants.PARAM_SOURCE.equals(nextParamName)) { 165 IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams( 166 myFhirContext, RestSearchParameterTypeEnum.URI, nextParamName, paramList); 167 paramMap.add(nextParamName, param); 168 } else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) { 169 paramMap.setDeleteExpunge(true); 170 } else if (Constants.PARAM_LIST.equals(nextParamName)) { 171 IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams( 172 myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList); 173 paramMap.add(nextParamName, param); 174 } else if (nextParamName.startsWith("_") && !Constants.PARAM_LANGUAGE.equals(nextParamName)) { 175 // ignore these since they aren't search params (e.g. _sort) 176 } else { 177 RuntimeSearchParam paramDef = 178 mySearchParamRegistry.getActiveSearchParam(theResourceDefinition.getName(), nextParamName); 179 if (paramDef == null) { 180 throw throwUnrecognizedParamException(theMatchUrl, theResourceDefinition, nextParamName); 181 } 182 183 IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams( 184 mySearchParamRegistry, myFhirContext, paramDef, nextParamName, paramList); 185 paramMap.add(nextParamName, param); 186 } 187 } 188 return paramMap; 189 } 190 191 public static class UnrecognizedSearchParameterException extends InvalidRequestException { 192 193 private final String myResourceName; 194 private final String myParamName; 195 196 UnrecognizedSearchParameterException(String theMessage, String theResourceName, String theParamName) { 197 super(theMessage); 198 myResourceName = theResourceName; 199 myParamName = theParamName; 200 } 201 202 public String getResourceName() { 203 return myResourceName; 204 } 205 206 public String getParamName() { 207 return myParamName; 208 } 209 } 210 211 private InvalidRequestException throwUnrecognizedParamException( 212 String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, String nextParamName) { 213 return new UnrecognizedSearchParameterException( 214 Msg.code(488) + "Failed to parse match URL[" + theMatchUrl + "] - Resource type " 215 + theResourceDefinition.getName() + " does not have a parameter with name: " + nextParamName, 216 theResourceDefinition.getName(), 217 nextParamName); 218 } 219 220 private IQueryParameterAnd<?> newInstanceAnd(String theParamType) { 221 Class<? extends IQueryParameterAnd<?>> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(theParamType); 222 return ReflectionUtil.newInstance(clazz); 223 } 224 225 public IQueryParameterType newInstanceType(String theParamType) { 226 Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(theParamType); 227 return ReflectionUtil.newInstance(clazz); 228 } 229 230 public ResourceSearch getResourceSearch(String theUrl, RequestPartitionId theRequestPartitionId) { 231 RuntimeResourceDefinition resourceDefinition; 232 resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theUrl); 233 SearchParameterMap searchParameterMap = translateMatchUrl(theUrl, resourceDefinition); 234 return new ResourceSearch(resourceDefinition, searchParameterMap, theRequestPartitionId); 235 } 236 237 public ResourceSearch getResourceSearch(String theUrl) { 238 return getResourceSearch(theUrl, null); 239 } 240 241 public abstract static class Flag { 242 243 /** 244 * Constructor 245 */ 246 Flag() { 247 // nothing 248 } 249 250 abstract void process( 251 String theParamName, List<QualifiedParamList> theValues, SearchParameterMap theMapToPopulate); 252 } 253 254 /** 255 * Indicates that the parser should process _include and _revinclude (by default these are not handled) 256 */ 257 public static Flag processIncludes() { 258 return new Flag() { 259 260 @Override 261 void process(String theParamName, List<QualifiedParamList> theValues, SearchParameterMap theMapToPopulate) { 262 if (Constants.PARAM_INCLUDE.equals(theParamName)) { 263 for (QualifiedParamList nextQualifiedList : theValues) { 264 for (String nextValue : nextQualifiedList) { 265 theMapToPopulate.addInclude(new Include( 266 nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier()))); 267 } 268 } 269 } else if (Constants.PARAM_REVINCLUDE.equals(theParamName)) { 270 for (QualifiedParamList nextQualifiedList : theValues) { 271 for (String nextValue : nextQualifiedList) { 272 theMapToPopulate.addRevInclude(new Include( 273 nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier()))); 274 } 275 } 276 } 277 } 278 }; 279 } 280}