
001/* 002 * #%L 003 * HAPI FHIR - Core Library 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.rest.param; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.model.api.IQueryParameterOr; 026import ca.uhn.fhir.model.api.IQueryParameterType; 027import ca.uhn.fhir.model.primitive.IdDt; 028import ca.uhn.fhir.model.primitive.IntegerDt; 029import ca.uhn.fhir.rest.annotation.IdParam; 030import ca.uhn.fhir.rest.api.Constants; 031import ca.uhn.fhir.rest.api.QualifiedParamList; 032import ca.uhn.fhir.util.ReflectionUtil; 033import ca.uhn.fhir.util.UrlUtil; 034import org.hl7.fhir.instance.model.api.IIdType; 035import org.hl7.fhir.instance.model.api.IPrimitiveType; 036 037import java.io.Serial; 038import java.lang.annotation.Annotation; 039import java.lang.reflect.Method; 040import java.util.ArrayList; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.List; 044import java.util.stream.Collectors; 045 046public class ParameterUtil { 047 048 @SuppressWarnings("unchecked") 049 public static <T extends IIdType> T convertIdToType(IIdType value, Class<T> theIdParamType) { 050 if (value != null && !theIdParamType.isAssignableFrom(value.getClass())) { 051 IIdType newValue = ReflectionUtil.newInstance(theIdParamType); 052 newValue.setValue(value.getValue()); 053 value = newValue; 054 } 055 return (T) value; 056 } 057 058 /** 059 * Removes :modifiers and .chains from URL parameter names 060 */ 061 public static String stripModifierPart(String theParam) { 062 for (int i = 0; i < theParam.length(); i++) { 063 char nextChar = theParam.charAt(i); 064 if (nextChar == ':' || nextChar == '.') { 065 return theParam.substring(0, i); 066 } 067 } 068 return theParam; 069 } 070 071 /** 072 * Escapes a string according to the rules for parameter escaping specified in the <a href="http://www.hl7.org/implement/standards/fhir/search.html#escaping">FHIR Specification Escaping 073 * Section</a> 074 */ 075 public static String escape(String theValue) { 076 if (theValue == null) { 077 return null; 078 } 079 StringBuilder b = new StringBuilder(); 080 081 for (int i = 0; i < theValue.length(); i++) { 082 char next = theValue.charAt(i); 083 switch (next) { 084 case '$': 085 case ',': 086 case '|': 087 case '\\': 088 b.append('\\'); 089 break; 090 default: 091 break; 092 } 093 b.append(next); 094 } 095 096 return b.toString(); 097 } 098 099 /** 100 * Escapes a string according to the rules for parameter escaping specified in the <a href="http://www.hl7.org/implement/standards/fhir/search.html#escaping">FHIR Specification Escaping 101 * Section</a> 102 */ 103 public static String escapeWithDefault(Object theValue) { 104 if (theValue == null) { 105 return ""; 106 } 107 return escape(theValue.toString()); 108 } 109 110 /** 111 * Applies {@link #escapeWithDefault(Object)} followed by {@link UrlUtil#escapeUrlParam(String)} 112 */ 113 public static String escapeAndUrlEncode(String theInput) { 114 return UrlUtil.escapeUrlParam(escapeWithDefault(theInput)); 115 } 116 117 public static Integer findIdParameterIndex(Method theMethod, FhirContext theContext) { 118 Integer index = findParamAnnotationIndex(theMethod, IdParam.class); 119 if (index != null) { 120 Class<?> paramType = theMethod.getParameterTypes()[index]; 121 if (IIdType.class.equals(paramType)) { 122 return index; 123 } 124 boolean isRi = theContext.getVersion().getVersion().isRi(); 125 boolean usesHapiId = IdDt.class.equals(paramType); 126 if (isRi == usesHapiId) { 127 throw new ConfigurationException(Msg.code(1936) 128 + "Method uses the wrong Id datatype (IdDt / IdType) for the given context FHIR version: " 129 + theMethod); 130 } 131 } 132 return index; 133 } 134 135 // public static Integer findSinceParameterIndex(Method theMethod) { 136 // return findParamIndex(theMethod, Since.class); 137 // } 138 139 public static Integer findParamAnnotationIndex(Method theMethod, Class<?> toFind) { 140 int paramIndex = 0; 141 for (Annotation[] annotations : theMethod.getParameterAnnotations()) { 142 for (Annotation nextAnnotation : annotations) { 143 Class<? extends Annotation> class1 = nextAnnotation.annotationType(); 144 if (toFind.isAssignableFrom(class1)) { 145 return paramIndex; 146 } 147 } 148 paramIndex++; 149 } 150 return null; 151 } 152 153 public static Object fromInteger(Class<?> theType, IntegerDt theArgument) { 154 if (theArgument == null) { 155 return null; 156 } 157 if (theType.equals(Integer.class)) { 158 return theArgument.getValue(); 159 } 160 IPrimitiveType<?> retVal = (IPrimitiveType<?>) ReflectionUtil.newInstance(theType); 161 retVal.setValueAsString(theArgument.getValueAsString()); 162 return retVal; 163 } 164 165 public static boolean isBindableIntegerType(Class<?> theClass) { 166 return Integer.class.isAssignableFrom(theClass) || IPrimitiveType.class.isAssignableFrom(theClass); 167 } 168 169 public static String escapeAndJoinOrList(Collection<String> theValues) { 170 return theValues.stream().map(ParameterUtil::escape).collect(Collectors.joining(",")); 171 } 172 173 public static int nonEscapedIndexOf(String theString, char theCharacter) { 174 for (int i = 0; i < theString.length(); i++) { 175 if (theString.charAt(i) == theCharacter) { 176 if (i == 0 || theString.charAt(i - 1) != '\\') { 177 return i; 178 } 179 } 180 } 181 return -1; 182 } 183 184 public static String parseETagValue(String value) { 185 String eTagVersion; 186 value = value.trim(); 187 if (value.length() > 1) { 188 if (value.charAt(value.length() - 1) == '"') { 189 if (value.charAt(0) == '"') { 190 eTagVersion = value.substring(1, value.length() - 1); 191 } else if (value.length() > 3 192 && value.charAt(0) == 'W' 193 && value.charAt(1) == '/' 194 && value.charAt(2) == '"') { 195 eTagVersion = value.substring(3, value.length() - 1); 196 } else { 197 eTagVersion = value; 198 } 199 } else { 200 eTagVersion = value; 201 } 202 } else { 203 eTagVersion = value; 204 } 205 return eTagVersion; 206 } 207 208 public static IQueryParameterOr<?> singleton(final IQueryParameterType theParam, final String theParamName) { 209 return new IQueryParameterOr<>() { 210 211 @Serial 212 private static final long serialVersionUID = 1L; 213 214 @Override 215 public List<IQueryParameterType> getValuesAsQueryTokens() { 216 return Collections.singletonList(theParam); 217 } 218 219 @Override 220 public void setValuesAsQueryTokens( 221 FhirContext theContext, String theParamName, QualifiedParamList theParameters) { 222 if (theParameters.isEmpty()) { 223 return; 224 } 225 if (theParameters.size() > 1) { 226 throw new IllegalArgumentException(Msg.code(1937) + "Type " 227 + theParam.getClass().getCanonicalName() + " does not support multiple values"); 228 } 229 theParam.setValueAsQueryToken( 230 theContext, theParamName, theParameters.getQualifier(), theParameters.get(0)); 231 } 232 }; 233 } 234 235 static List<String> splitParameterString(String theInput, char theDelimiter, boolean theUnescapeComponents) { 236 ArrayList<String> retVal = new ArrayList<>(); 237 if (theInput != null) { 238 StringBuilder b = new StringBuilder(); 239 for (int i = 0; i < theInput.length(); i++) { 240 char next = theInput.charAt(i); 241 if (next == theDelimiter) { 242 if (i == 0) { 243 b.append(next); 244 } else { 245 char prevChar = theInput.charAt(i - 1); 246 if (prevChar == '\\') { 247 b.append(next); 248 } else { 249 if (b.length() > 0) { 250 retVal.add(b.toString()); 251 } else { 252 retVal.add(null); 253 } 254 b.setLength(0); 255 } 256 } 257 } else { 258 b.append(next); 259 } 260 } 261 if (b.length() > 0) { 262 retVal.add(b.toString()); 263 } 264 } 265 266 if (theUnescapeComponents) { 267 retVal.replaceAll(ParameterUtil::unescape); 268 } 269 270 return retVal; 271 } 272 273 public static IntegerDt toInteger(Object theArgument) { 274 if (theArgument instanceof IntegerDt) { 275 return (IntegerDt) theArgument; 276 } 277 if (theArgument instanceof Integer) { 278 return new IntegerDt((Integer) theArgument); 279 } 280 if (theArgument instanceof IPrimitiveType<?> pt) { 281 return new IntegerDt(pt.getValueAsString()); 282 } 283 return null; 284 } 285 286 /** 287 * Unescapes a string according to the rules for parameter escaping specified in the <a href="http://www.hl7.org/implement/standards/fhir/search.html#escaping">FHIR Specification Escaping 288 * Section</a> 289 */ 290 public static String unescape(String theValue) { 291 if (theValue == null) { 292 return null; 293 } 294 if (theValue.indexOf('\\') == -1) { 295 return theValue; 296 } 297 298 StringBuilder b = new StringBuilder(); 299 300 for (int i = 0; i < theValue.length(); i++) { 301 char next = theValue.charAt(i); 302 if (next == '\\') { 303 if (i == theValue.length() - 1) { 304 b.append(next); 305 } else { 306 char nextPlusOne = theValue.charAt(i + 1); 307 switch (nextPlusOne) { 308 case '$': 309 case ',': 310 case '|': 311 case '\\': 312 b.append(nextPlusOne); 313 i++; 314 break; 315 default: 316 b.append(next); 317 } 318 } 319 } else { 320 b.append(next); 321 } 322 } 323 324 return b.toString(); 325 } 326 327 /** 328 * Returns true if the value is :iterate or :recurse (the former name of :iterate) for an _include parameter 329 */ 330 public static boolean isIncludeIterate(String theQualifier) { 331 return Constants.PARAM_INCLUDE_QUALIFIER_RECURSE.equals(theQualifier) 332 || Constants.PARAM_INCLUDE_QUALIFIER_ITERATE.equals(theQualifier); 333 } 334 335 /** 336 * Given a list of query parameters, returns {@literal true} if all parameters in the list 337 * return {@literal true} from {@link IQueryParameterType#isEmpty()}, or if the list 338 * does not contain any elements. 339 * 340 * @since 8.4.0 341 */ 342 public static boolean areAllParametersEmpty(List<? extends IQueryParameterType> theList) { 343 for (IQueryParameterType next : theList) { 344 if (!next.isEmpty()) { 345 return false; 346 } 347 } 348 return true; 349 } 350}