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