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}