001package ca.uhn.fhir.rest.param;
002
003import ca.uhn.fhir.context.ConfigurationException;
004import ca.uhn.fhir.context.FhirContext;
005import ca.uhn.fhir.context.RuntimeSearchParam;
006import ca.uhn.fhir.model.api.IQueryParameterAnd;
007import ca.uhn.fhir.model.api.IQueryParameterOr;
008import ca.uhn.fhir.model.api.IQueryParameterType;
009import ca.uhn.fhir.model.primitive.IdDt;
010import ca.uhn.fhir.model.primitive.IntegerDt;
011import ca.uhn.fhir.rest.annotation.IdParam;
012import ca.uhn.fhir.rest.api.QualifiedParamList;
013import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
014import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
015import ca.uhn.fhir.util.ReflectionUtil;
016import ca.uhn.fhir.util.UrlUtil;
017import org.hl7.fhir.instance.model.api.IIdType;
018import org.hl7.fhir.instance.model.api.IPrimitiveType;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Method;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.stream.Collectors;
027
028/*
029 * #%L
030 * HAPI FHIR - Core Library
031 * %%
032 * Copyright (C) 2014 - 2020 University Health Network
033 * %%
034 * Licensed under the Apache License, Version 2.0 (the "License");
035 * you may not use this file except in compliance with the License.
036 * You may obtain a copy of the License at
037 *
038 * http://www.apache.org/licenses/LICENSE-2.0
039 *
040 * Unless required by applicable law or agreed to in writing, software
041 * distributed under the License is distributed on an "AS IS" BASIS,
042 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
043 * See the License for the specific language governing permissions and
044 * limitations under the License.
045 * #L%
046 */
047
048public class ParameterUtil {
049
050        @SuppressWarnings("unchecked")
051        public static <T extends IIdType> T convertIdToType(IIdType value, Class<T> theIdParamType) {
052                if (value != null && !theIdParamType.isAssignableFrom(value.getClass())) {
053                        IIdType newValue = ReflectionUtil.newInstance(theIdParamType);
054                        newValue.setValue(value.getValue());
055                        value = newValue;
056                }
057                return (T) value;
058        }
059
060        /**
061         * This is a utility method intended provided to help the JPA module.
062         */
063        public static IQueryParameterAnd<?> parseQueryParams(FhirContext theContext, RestSearchParameterTypeEnum paramType,
064                                                                                                                                                  String theUnqualifiedParamName, List<QualifiedParamList> theParameters) {
065                QueryParameterAndBinder binder;
066                switch (paramType) {
067                        case COMPOSITE:
068                                throw new UnsupportedOperationException();
069                        case DATE:
070                                binder = new QueryParameterAndBinder(DateAndListParam.class,
071                                        Collections.emptyList());
072                                break;
073                        case NUMBER:
074                                binder = new QueryParameterAndBinder(NumberAndListParam.class,
075                                        Collections.emptyList());
076                                break;
077                        case QUANTITY:
078                                binder = new QueryParameterAndBinder(QuantityAndListParam.class,
079                                        Collections.emptyList());
080                                break;
081                        case REFERENCE:
082                                binder = new QueryParameterAndBinder(ReferenceAndListParam.class,
083                                        Collections.emptyList());
084                                break;
085                        case STRING:
086                                binder = new QueryParameterAndBinder(StringAndListParam.class,
087                                        Collections.emptyList());
088                                break;
089                        case TOKEN:
090                                binder = new QueryParameterAndBinder(TokenAndListParam.class,
091                                        Collections.emptyList());
092                                break;
093                        case URI:
094                                binder = new QueryParameterAndBinder(UriAndListParam.class,
095                                        Collections.emptyList());
096                                break;
097                        case HAS:
098                                binder = new QueryParameterAndBinder(HasAndListParam.class,
099                                        Collections.emptyList());
100                                break;
101                        case SPECIAL:
102                                binder = new QueryParameterAndBinder(SpecialAndListParam.class,
103                                        Collections.emptyList());
104                                break;
105                        default:
106                                throw new IllegalArgumentException("Parameter '" + theUnqualifiedParamName + "' has type " + paramType + " which is currently not supported.");
107                }
108
109                return binder.parse(theContext, theUnqualifiedParamName, theParameters);
110        }
111
112        /**
113         * This is a utility method intended provided to help the JPA module.
114         */
115        public static IQueryParameterAnd<?> parseQueryParams(FhirContext theContext, RuntimeSearchParam theParamDef,
116                                                                                                                                                  String theUnqualifiedParamName, List<QualifiedParamList> theParameters) {
117                RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
118                return parseQueryParams(theContext, paramType, theUnqualifiedParamName, theParameters);
119        }
120
121        /**
122         * 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
123         * Section</a>
124         */
125        public static String escape(String theValue) {
126                if (theValue == null) {
127                        return null;
128                }
129                StringBuilder b = new StringBuilder();
130
131                for (int i = 0; i < theValue.length(); i++) {
132                        char next = theValue.charAt(i);
133                        switch (next) {
134                                case '$':
135                                case ',':
136                                case '|':
137                                case '\\':
138                                        b.append('\\');
139                                        break;
140                                default:
141                                        break;
142                        }
143                        b.append(next);
144                }
145
146                return b.toString();
147        }
148
149        /**
150         * 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
151         * Section</a>
152         */
153        public static String escapeWithDefault(Object theValue) {
154                if (theValue == null) {
155                        return "";
156                }
157                return escape(theValue.toString());
158        }
159
160        /**
161         * Applies {@link #escapeWithDefault(Object)} followed by {@link UrlUtil#escapeUrlParam(String)}
162         */
163        public static String escapeAndUrlEncode(String theInput) {
164                return UrlUtil.escapeUrlParam(escapeWithDefault(theInput));
165        }
166
167        public static Integer findIdParameterIndex(Method theMethod, FhirContext theContext) {
168                Integer index = findParamAnnotationIndex(theMethod, IdParam.class);
169                if (index != null) {
170                        Class<?> paramType = theMethod.getParameterTypes()[index];
171                        if (IIdType.class.equals(paramType)) {
172                                return index;
173                        }
174                        boolean isRi = theContext.getVersion().getVersion().isRi();
175                        boolean usesHapiId = IdDt.class.equals(paramType);
176                        if (isRi == usesHapiId) {
177                                throw new ConfigurationException("Method uses the wrong Id datatype (IdDt / IdType) for the given context FHIR version: " + theMethod.toString());
178                        }
179                }
180                return index;
181        }
182
183        // public static Integer findSinceParameterIndex(Method theMethod) {
184        // return findParamIndex(theMethod, Since.class);
185        // }
186
187        public static Integer findParamAnnotationIndex(Method theMethod, Class<?> toFind) {
188                int paramIndex = 0;
189                for (Annotation[] annotations : theMethod.getParameterAnnotations()) {
190                        for (Annotation nextAnnotation : annotations) {
191                                Class<? extends Annotation> class1 = nextAnnotation.annotationType();
192                                if (toFind.isAssignableFrom(class1)) {
193                                        return paramIndex;
194                                }
195                        }
196                        paramIndex++;
197                }
198                return null;
199        }
200
201        public static Object fromInteger(Class<?> theType, IntegerDt theArgument) {
202                if (theArgument == null) {
203                        return null;
204                }
205                if (theType.equals(Integer.class)) {
206                        return theArgument.getValue();
207                }
208                IPrimitiveType<?> retVal = (IPrimitiveType<?>) ReflectionUtil.newInstance(theType);
209                retVal.setValueAsString(theArgument.getValueAsString());
210                return retVal;
211        }
212
213        public static boolean isBindableIntegerType(Class<?> theClass) {
214                return Integer.class.isAssignableFrom(theClass)
215                        || IPrimitiveType.class.isAssignableFrom(theClass);
216        }
217
218        public static String escapeAndJoinOrList(Collection<String> theValues) {
219                return theValues
220                        .stream()
221                        .map(ParameterUtil::escape)
222                        .collect(Collectors.joining(","));
223        }
224
225        public static int nonEscapedIndexOf(String theString, char theCharacter) {
226                for (int i = 0; i < theString.length(); i++) {
227                        if (theString.charAt(i) == theCharacter) {
228                                if (i == 0 || theString.charAt(i - 1) != '\\') {
229                                        return i;
230                                }
231                        }
232                }
233                return -1;
234        }
235
236        public static String parseETagValue(String value) {
237                String eTagVersion;
238                value = value.trim();
239                if (value.length() > 1) {
240                        if (value.charAt(value.length() - 1) == '"') {
241                                if (value.charAt(0) == '"') {
242                                        eTagVersion = value.substring(1, value.length() - 1);
243                                } else if (value.length() > 3 && value.charAt(0) == 'W' && value.charAt(1) == '/'
244                                        && value.charAt(2) == '"') {
245                                        eTagVersion = value.substring(3, value.length() - 1);
246                                } else {
247                                        eTagVersion = value;
248                                }
249                        } else {
250                                eTagVersion = value;
251                        }
252                } else {
253                        eTagVersion = value;
254                }
255                return eTagVersion;
256        }
257
258        public static IQueryParameterOr<?> singleton(final IQueryParameterType theParam, final String theParamName) {
259                return new IQueryParameterOr<IQueryParameterType>() {
260
261                        private static final long serialVersionUID = 1L;
262
263                        @Override
264                        public List<IQueryParameterType> getValuesAsQueryTokens() {
265                                return Collections.singletonList(theParam);
266                        }
267
268                        @Override
269                        public void setValuesAsQueryTokens(FhirContext theContext, String theParamName,
270                                                                                                                  QualifiedParamList theParameters) {
271                                if (theParameters.isEmpty()) {
272                                        return;
273                                }
274                                if (theParameters.size() > 1) {
275                                        throw new IllegalArgumentException(
276                                                "Type " + theParam.getClass().getCanonicalName() + " does not support multiple values");
277                                }
278                                theParam.setValueAsQueryToken(theContext, theParamName, theParameters.getQualifier(),
279                                        theParameters.get(0));
280                        }
281                };
282        }
283
284        static List<String> splitParameterString(String theInput, char theDelimiter, boolean theUnescapeComponents) {
285                ArrayList<String> retVal = new ArrayList<>();
286                if (theInput != null) {
287                        StringBuilder b = new StringBuilder();
288                        for (int i = 0; i < theInput.length(); i++) {
289                                char next = theInput.charAt(i);
290                                if (next == theDelimiter) {
291                                        if (i == 0) {
292                                                b.append(next);
293                                        } else {
294                                                char prevChar = theInput.charAt(i - 1);
295                                                if (prevChar == '\\') {
296                                                        b.append(next);
297                                                } else {
298                                                        if (b.length() > 0) {
299                                                                retVal.add(b.toString());
300                                                        } else {
301                                                                retVal.add(null);
302                                                        }
303                                                        b.setLength(0);
304                                                }
305                                        }
306                                } else {
307                                        b.append(next);
308                                }
309                        }
310                        if (b.length() > 0) {
311                                retVal.add(b.toString());
312                        }
313                }
314
315                if (theUnescapeComponents) {
316                        for (int i = 0; i < retVal.size(); i++) {
317                                retVal.set(i, unescape(retVal.get(i)));
318                        }
319                }
320
321                return retVal;
322        }
323
324        public static IntegerDt toInteger(Object theArgument) {
325                if (theArgument instanceof IntegerDt) {
326                        return (IntegerDt) theArgument;
327                }
328                if (theArgument instanceof Integer) {
329                        return new IntegerDt((Integer) theArgument);
330                }
331                if (theArgument instanceof IPrimitiveType) {
332                        IPrimitiveType<?> pt = (IPrimitiveType<?>) theArgument;
333                        return new IntegerDt(pt.getValueAsString());
334                }
335                return null;
336        }
337
338        /**
339         * 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
340         * Section</a>
341         */
342        public static String unescape(String theValue) {
343                if (theValue == null) {
344                        return null;
345                }
346                if (theValue.indexOf('\\') == -1) {
347                        return theValue;
348                }
349
350                StringBuilder b = new StringBuilder();
351
352                for (int i = 0; i < theValue.length(); i++) {
353                        char next = theValue.charAt(i);
354                        if (next == '\\') {
355                                if (i == theValue.length() - 1) {
356                                        b.append(next);
357                                } else {
358                                        switch (theValue.charAt(i + 1)) {
359                                                case '$':
360                                                case ',':
361                                                case '|':
362                                                case '\\':
363                                                        continue;
364                                                default:
365                                                        b.append(next);
366                                        }
367                                }
368                        } else {
369                                b.append(next);
370                        }
371                }
372
373                return b.toString();
374        }
375
376}