-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathParamConvertUtils.java
419 lines (371 loc) · 13.4 KB
/
ParamConvertUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package io.cdap.http.internal;
import io.netty.handler.codec.http.cookie.Cookie;
import org.apache.commons.beanutils.ConvertUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.ext.ParamConverterProvider;
/**
* Util class to convert request parameters.
*/
public final class ParamConvertUtils {
private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS;
static {
Map<Class<?>, Object> defaults = new IdentityHashMap<>();
defaults.put(Boolean.TYPE, false);
defaults.put(Character.TYPE, '\0');
defaults.put(Byte.TYPE, (byte) 0);
defaults.put(Short.TYPE, (short) 0);
defaults.put(Integer.TYPE, 0);
defaults.put(Long.TYPE, 0L);
defaults.put(Float.TYPE, 0f);
defaults.put(Double.TYPE, 0d);
PRIMITIVE_DEFAULTS = Collections.unmodifiableMap(defaults);
}
/**
* Creates a converter function that converts a path segment into the given result type.
* Current implementation doesn't follow the {@link PathParam} specification to maintain backward compatibility.
*/
public static Converter<String, Object> createPathParamConverter(final Type resultType) {
if (!(resultType instanceof Class)) {
throw new IllegalArgumentException("Unsupported @PathParam type " + resultType);
}
return new Converter<String, Object>() {
@Override
public Object convert(String value) {
return ConvertUtils.convert(value, (Class<?>) resultType);
}
};
}
/**
* Creates a converter function that converts header value into an object of the given result type.
* It follows the supported types of {@link HeaderParam} with the following exceptions:
* <ol>
* <li>Does not support types registered with {@link ParamConverterProvider}</li>
* </ol>
*/
public static Converter<List<String>, Object> createHeaderParamConverter(Type resultType) {
return createListConverter(resultType);
}
/**
* Creates a converter function that converts cookie value into an object of the given result type.
* Currently, only {@link String} result types are supported.
*/
public static Converter<Cookie, Object> createCookieParamConverter(Type resultType) {
Class<?> resultClass = getRawClass(resultType);
// For string, return the cookie value
if (resultClass == String.class) {
return new Converter<Cookie, Object>() {
@Nullable
@Override
public Object convert(Cookie from) throws Exception {
return from.value();
}
};
}
throw new IllegalArgumentException("Unsupported CookieParam type " + resultType);
}
/**
* Creates a converter function that converts query parameter into an object of the given result type.
* It follows the supported types of {@link QueryParam} with the following exceptions:
* <ol>
* <li>Does not support types registered with {@link ParamConverterProvider}</li>
* </ol>
*/
public static Converter<List<String>, Object> createQueryParamConverter(Type resultType) {
return createListConverter(resultType);
}
/**
* Common helper method to convert value for {@link HeaderParam} and {@link QueryParam}.
*
* @see #createHeaderParamConverter(Type)
* @see #createQueryParamConverter(Type)
*/
private static Converter<List<String>, Object> createListConverter(Type resultType) {
// Use boxed type if raw type is primitive type. Otherwise the type won't change.
Class<?> resultClass = getRawClass(resultType);
// For string, just return the first value
if (resultClass == String.class) {
return new BasicConverter(defaultValue(resultClass)) {
@Override
protected Object convert(String value) throws Exception {
return value;
}
};
}
// Creates converter based on the type
// Primitive
Converter<List<String>, Object> converter = createPrimitiveTypeConverter(resultClass);
if (converter != null) {
return converter;
}
// String constructor
converter = createStringConstructorConverter(resultClass);
if (converter != null) {
return converter;
}
// Static string argument methods
converter = createStringMethodConverter(resultClass);
if (converter != null) {
return converter;
}
// Collection
converter = createCollectionConverter(resultType);
if (converter != null) {
return converter;
}
throw new IllegalArgumentException("Unsupported type " + resultType + " of type class " + resultType.getClass());
}
/**
* Creates a converter function that converts value into primitive type.
*
* @return A converter function or {@code null} if the given type is not primitive type or boxed type
*/
@Nullable
private static Converter<List<String>, Object> createPrimitiveTypeConverter(final Class<?> resultClass) {
Object defaultValue = defaultValue(resultClass);
if (defaultValue == null) {
// For primitive type, the default value shouldn't be null
return null;
}
return new BasicConverter(defaultValue) {
@Override
protected Object convert(String value) throws Exception {
return valueOf(value, resultClass);
}
};
}
/**
* Creates a converter function that converts value using a constructor that accepts a single String argument.
*
* @return A converter function or {@code null} if the given type doesn't have a public constructor that accepts
* a single String argument.
*/
private static Converter<List<String>, Object> createStringConstructorConverter(Class<?> resultClass) {
try {
final Constructor<?> constructor = resultClass.getConstructor(String.class);
return new BasicConverter(defaultValue(resultClass)) {
@Override
protected Object convert(String value) throws Exception {
return constructor.newInstance(value);
}
};
} catch (Exception e) {
return null;
}
}
/**
* Creates a converter function that converts value using a public static method named
* {@code valueOf} or {@code fromString} that accepts a single String argument.
*
* @return A converter function or {@code null} if the given type doesn't have a public static method
* named {@code valueOf} or {@code fromString} that accepts a single String argument.
*/
private static Converter<List<String>, Object> createStringMethodConverter(Class<?> resultClass) {
// A special case for Character type, as it doesn't have a valueOf(String) method
if (resultClass == Character.class) {
return new BasicConverter(defaultValue(resultClass)) {
@Nullable
@Override
Object convert(String value) throws Exception {
return value.length() >= 1 ? value.charAt(0) : null;
}
};
}
Method method;
try {
method = resultClass.getMethod("valueOf", String.class);
} catch (Exception e) {
try {
method = resultClass.getMethod("fromString", String.class);
} catch (Exception ex) {
return null;
}
}
final Method convertMethod = method;
return new BasicConverter(defaultValue(resultClass)) {
@Override
protected Object convert(String value) throws Exception {
return convertMethod.invoke(null, value);
}
};
}
/**
* Creates a converter function that converts value into a {@link List}, {@link Set} or {@link SortedSet}.
*
* @return A converter function or {@code null} if the given type is not a {@link ParameterizedType} with raw type as
* {@link List}, {@link Set} or {@link SortedSet}. Also, for {@link SortedSet} type, if the element type
* doesn't implements {@link Comparable}, {@code null} is returned.
*/
private static Converter<List<String>, Object> createCollectionConverter(Type resultType) {
final Class<?> rawType = getRawClass(resultType);
// Collection. It must be List or Set
if (rawType != List.class && rawType != Set.class && rawType != SortedSet.class) {
return null;
}
// Must be ParameterizedType
if (!(resultType instanceof ParameterizedType)) {
return null;
}
// Must have 1 type parameter
ParameterizedType type = (ParameterizedType) resultType;
if (type.getActualTypeArguments().length != 1) {
return null;
}
// For SortedSet, the entry type must be Comparable.
Type elementType = type.getActualTypeArguments()[0];
if (rawType == SortedSet.class && !Comparable.class.isAssignableFrom(getRawClass(elementType))) {
return null;
}
// Get the converter for the collection element.
final Converter<List<String>, Object> elementConverter = createQueryParamConverter(elementType);
if (elementConverter == null) {
return null;
}
return new Converter<List<String>, Object>() {
@Override
public Object convert(List<String> values) throws Exception {
Collection<? extends Comparable> collection;
if (rawType == List.class) {
collection = new ArrayList<>();
} else if (rawType == Set.class) {
collection = new LinkedHashSet<>();
} else {
collection = new TreeSet<>();
}
for (String value : values) {
add(collection, elementConverter.convert(Collections.singletonList(value)));
}
return collection;
}
@SuppressWarnings("unchecked")
private <T> void add(Collection<T> builder, Object element) {
builder.add((T) element);
}
};
}
/**
* Returns the default value for the given class type based on the Java language definition.
*/
@Nullable
private static Object defaultValue(Class<?> cls) {
return PRIMITIVE_DEFAULTS.get(cls);
}
/**
* Returns the value of the primitive type from the given string value.
*
* @param value the value to parse
* @param cls the primitive type class
* @return the boxed type value or {@code null} if the given class is not a primitive type
*/
@Nullable
private static Object valueOf(String value, Class<?> cls) {
if (cls == Boolean.TYPE) {
return Boolean.valueOf(value);
}
if (cls == Character.TYPE) {
return value.length() >= 1 ? value.charAt(0) : defaultValue(char.class);
}
if (cls == Byte.TYPE) {
return Byte.valueOf(value);
}
if (cls == Short.TYPE) {
return Short.valueOf(value);
}
if (cls == Integer.TYPE) {
return Integer.valueOf(value);
}
if (cls == Long.TYPE) {
return Long.valueOf(value);
}
if (cls == Float.TYPE) {
return Float.valueOf(value);
}
if (cls == Double.TYPE) {
return Double.valueOf(value);
}
return null;
}
/**
* Returns the raw class of the given type.
*/
private static Class<?> getRawClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
}
if (type instanceof ParameterizedType) {
return getRawClass(((ParameterizedType) type).getRawType());
}
// For TypeVariable and WildcardType, returns the first upper bound.
if (type instanceof TypeVariable) {
return getRawClass(((TypeVariable) type).getBounds()[0]);
}
if (type instanceof WildcardType) {
return getRawClass(((WildcardType) type).getUpperBounds()[0]);
}
if (type instanceof GenericArrayType) {
Class<?> componentClass = getRawClass(((GenericArrayType) type).getGenericComponentType());
return Array.newInstance(componentClass, 0).getClass();
}
// This shouldn't happen as we captured all implementations of Type above (as or Java 8)
throw new IllegalArgumentException("Unsupported type " + type + " of type class " + type.getClass());
}
/**
* A converter that converts first String value from a List of String.
*/
private abstract static class BasicConverter implements Converter<List<String>, Object> {
private final Object defaultValue;
BasicConverter(Object defaultValue) {
this.defaultValue = defaultValue;
}
@Nullable
@Override
public final Object convert(List<String> values) throws Exception {
if (values.isEmpty()) {
return getDefaultValue();
}
return convert(values.get(0));
}
Object getDefaultValue() {
return defaultValue;
}
@Nullable
abstract Object convert(String value) throws Exception;
}
private ParamConvertUtils() {
}
}