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