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