1
2
3
4
5
6 package mockit.internal.reflection;
7
8 import edu.umd.cs.findbugs.annotations.NonNull;
9 import edu.umd.cs.findbugs.annotations.Nullable;
10
11 import java.lang.reflect.GenericArrayType;
12 import java.lang.reflect.GenericDeclaration;
13 import java.lang.reflect.Member;
14 import java.lang.reflect.ParameterizedType;
15 import java.lang.reflect.Type;
16 import java.lang.reflect.TypeVariable;
17 import java.lang.reflect.WildcardType;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Map.Entry;
24
25 @SuppressWarnings("OverlyComplexClass")
26 public final class GenericTypeReflection {
27 @NonNull
28 private final Map<String, Type> typeParametersToTypeArguments;
29 @NonNull
30 private final Map<String, String> typeParametersToTypeArgumentNames;
31 private final boolean withSignatures;
32
33 public GenericTypeReflection(@NonNull Class<?> ownerClass, @Nullable Type genericType) {
34 this(ownerClass, genericType, true);
35 }
36
37 public GenericTypeReflection(@NonNull Class<?> ownerClass, @Nullable Type genericType, boolean withSignatures) {
38 typeParametersToTypeArguments = new HashMap<>(4);
39 typeParametersToTypeArgumentNames = withSignatures ? new HashMap<>(4) : Collections.<String, String> emptyMap();
40 this.withSignatures = withSignatures;
41 discoverTypeMappings(ownerClass, genericType);
42 }
43
44 private void discoverTypeMappings(@NonNull Class<?> rawType, @Nullable Type genericType) {
45 if (genericType instanceof ParameterizedType) {
46 addMappingsFromTypeParametersToTypeArguments(rawType, (ParameterizedType) genericType);
47 }
48
49 addGenericTypeMappingsForSuperTypes(rawType);
50 }
51
52 private void addGenericTypeMappingsForSuperTypes(@NonNull Class<?> rawType) {
53 Type superType = rawType;
54
55 while (superType != null && superType != Object.class) {
56 Class<?> superClass = (Class<?>) superType;
57 superType = superClass.getGenericSuperclass();
58
59 if (superType != null && superType != Object.class) {
60 superClass = addGenericTypeMappingsIfParameterized(superType);
61 superType = superClass;
62 }
63
64 addGenericTypeMappingsForInterfaces(superClass);
65 }
66 }
67
68 @NonNull
69 private Class<?> addGenericTypeMappingsIfParameterized(@NonNull Type superType) {
70 if (superType instanceof ParameterizedType) {
71 ParameterizedType genericSuperType = (ParameterizedType) superType;
72 Class<?> rawType = (Class<?>) genericSuperType.getRawType();
73 addMappingsFromTypeParametersToTypeArguments(rawType, genericSuperType);
74 return rawType;
75 }
76
77 return (Class<?>) superType;
78 }
79
80 private void addGenericTypeMappingsForInterfaces(@NonNull Class<?> classOrInterface) {
81 for (Type implementedInterface : classOrInterface.getGenericInterfaces()) {
82 Class<?> implementedType = addGenericTypeMappingsIfParameterized(implementedInterface);
83 addGenericTypeMappingsForInterfaces(implementedType);
84 }
85 }
86
87 private void addMappingsFromTypeParametersToTypeArguments(@NonNull Class<?> rawType,
88 @NonNull ParameterizedType genericType) {
89 String ownerTypeDesc = getOwnerClassDesc(rawType);
90 TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
91 Type[] typeArguments = genericType.getActualTypeArguments();
92
93 for (int i = 0, n = typeParameters.length; i < n; i++) {
94 TypeVariable<?> typeParam = typeParameters[i];
95 String typeVarName = typeParam.getName();
96
97 if (typeParametersToTypeArguments.containsKey(ownerTypeDesc + ':' + typeVarName)) {
98 continue;
99 }
100
101 Type typeArg = typeArguments[i];
102
103 if (typeArg instanceof Class<?>) {
104 addMappingForClassType(ownerTypeDesc, typeVarName, typeArg);
105 } else if (typeArg instanceof TypeVariable<?>) {
106 addMappingForTypeVariable(ownerTypeDesc, typeVarName, typeArg);
107 } else if (typeArg instanceof ParameterizedType) {
108 addMappingForParameterizedType(ownerTypeDesc, typeVarName, typeArg);
109 } else if (typeArg instanceof GenericArrayType) {
110 addMappingForArrayType(ownerTypeDesc, typeVarName, typeArg);
111 } else {
112 addMappingForFirstTypeBound(ownerTypeDesc, typeParam);
113 }
114 }
115
116 Type outerType = genericType.getOwnerType();
117
118 if (outerType instanceof ParameterizedType) {
119 ParameterizedType parameterizedOuterType = (ParameterizedType) outerType;
120 Class<?> rawOuterType = (Class<?>) parameterizedOuterType.getRawType();
121 addMappingsFromTypeParametersToTypeArguments(rawOuterType, parameterizedOuterType);
122 }
123 }
124
125 private void addMappingForClassType(@NonNull String ownerTypeDesc, @NonNull String typeName,
126 @NonNull Type typeArg) {
127 String mappedTypeArgName = null;
128
129 if (withSignatures) {
130 Class<?> classArg = (Class<?>) typeArg;
131 String ownerClassDesc = getOwnerClassDesc(classArg);
132 mappedTypeArgName = classArg.isArray() ? ownerClassDesc : 'L' + ownerClassDesc;
133 }
134
135 addTypeMapping(ownerTypeDesc, typeName, typeArg, mappedTypeArgName);
136 }
137
138 private void addMappingForTypeVariable(@NonNull String ownerTypeDesc, @NonNull String typeName,
139 @NonNull Type typeArg) {
140 @Nullable
141 String mappedTypeArgName = null;
142
143 if (withSignatures) {
144 TypeVariable<?> typeVar = (TypeVariable<?>) typeArg;
145 String ownerClassDesc = getOwnerClassDesc(typeVar);
146 String intermediateTypeArg = ownerClassDesc + ":T" + typeVar.getName();
147 mappedTypeArgName = typeParametersToTypeArgumentNames.get(intermediateTypeArg);
148 }
149
150 addTypeMapping(ownerTypeDesc, typeName, typeArg, mappedTypeArgName);
151 }
152
153 private void addMappingForParameterizedType(@NonNull String ownerTypeDesc, @NonNull String typeName,
154 @NonNull Type typeArg) {
155 String mappedTypeArgName = getMappedTypeArgName(typeArg);
156 addTypeMapping(ownerTypeDesc, typeName, typeArg, mappedTypeArgName);
157 }
158
159 @Nullable
160 private String getMappedTypeArgName(@NonNull Type typeArg) {
161 if (withSignatures) {
162 Class<?> classType = getClassType(typeArg);
163 return 'L' + getOwnerClassDesc(classType);
164 }
165
166 return null;
167 }
168
169 private void addMappingForArrayType(@NonNull String ownerTypeDesc, @NonNull String typeName,
170 @NonNull Type typeArg) {
171 String mappedTypeArgName = null;
172
173 if (withSignatures) {
174 mappedTypeArgName = getMappedTypeArgName((GenericArrayType) typeArg);
175 }
176
177 addTypeMapping(ownerTypeDesc, typeName, typeArg, mappedTypeArgName);
178 }
179
180 private void addMappingForFirstTypeBound(@NonNull String ownerTypeDesc, @NonNull TypeVariable<?> typeParam) {
181 Type typeArg = typeParam.getBounds()[0];
182 String mappedTypeArgName = getMappedTypeArgName(typeArg);
183 addTypeMapping(ownerTypeDesc, typeParam.getName(), typeArg, mappedTypeArgName);
184 }
185
186 @NonNull
187 private static String getOwnerClassDesc(@NonNull Class<?> rawType) {
188 return rawType.getName().replace('.', '/');
189 }
190
191 @NonNull
192 private Class<?> getClassType(@NonNull Type type) {
193 if (type instanceof ParameterizedType) {
194 ParameterizedType parameterizedType = (ParameterizedType) type;
195 return (Class<?>) parameterizedType.getRawType();
196 }
197
198 if (type instanceof TypeVariable<?>) {
199 TypeVariable<?> typeVar = (TypeVariable<?>) type;
200 String typeVarKey = getTypeVariableKey(typeVar);
201 @Nullable
202 Type typeArg = typeParametersToTypeArguments.get(typeVarKey);
203
204 if (typeArg == null) {
205 throw new IllegalArgumentException("Unable to resolve type variable \"" + typeVar.getName() + '"');
206 }
207
208
209 return getClassType(typeArg);
210 }
211
212 return (Class<?>) type;
213 }
214
215 @NonNull
216 private String getMappedTypeArgName(@NonNull GenericArrayType arrayType) {
217 StringBuilder argName = new StringBuilder(20);
218 argName.append('[');
219
220 while (true) {
221 Type componentType = arrayType.getGenericComponentType();
222
223 if (!(componentType instanceof GenericArrayType)) {
224 Class<?> classType = getClassType(componentType);
225 argName.append('L').append(getOwnerClassDesc(classType));
226 return argName.toString();
227 }
228 argName.append('[');
229
230 arrayType = (GenericArrayType) componentType;
231 }
232 }
233
234 private void addTypeMapping(@NonNull String ownerTypeDesc, @NonNull String typeVarName, @NonNull Type mappedTypeArg,
235 @Nullable String mappedTypeArgName) {
236 typeParametersToTypeArguments.put(ownerTypeDesc + ':' + typeVarName, mappedTypeArg);
237
238 if (mappedTypeArgName != null) {
239 addTypeMapping(ownerTypeDesc, typeVarName, mappedTypeArgName);
240 }
241 }
242
243 private void addTypeMapping(@NonNull String ownerTypeDesc, @NonNull String typeVarName,
244 @NonNull String mappedTypeArgName) {
245 String typeMappingKey = ownerTypeDesc + ":T" + typeVarName;
246 typeParametersToTypeArgumentNames.put(typeMappingKey, mappedTypeArgName);
247 }
248
249 public final class GenericSignature {
250 private final List<String> parameters = new ArrayList<>();
251 private final String parameterTypeDescs;
252 private final int lengthOfParameterTypeDescs;
253 private int currentPos;
254
255 GenericSignature(@NonNull String signature) {
256 int p = signature.indexOf('(');
257 int q = signature.lastIndexOf(')');
258 parameterTypeDescs = signature.substring(p + 1, q);
259 lengthOfParameterTypeDescs = parameterTypeDescs.length();
260 addTypeDescsToList();
261 }
262
263 private void addTypeDescsToList() {
264 while (currentPos < lengthOfParameterTypeDescs) {
265 addNextParameter();
266 }
267 }
268
269 private void addNextParameter() {
270 int startPos = currentPos;
271 int endPos;
272 char c = parameterTypeDescs.charAt(startPos);
273
274 switch (c) {
275 case 'T':
276 endPos = parameterTypeDescs.indexOf(';', startPos);
277 currentPos = endPos;
278 break;
279 case 'L':
280 endPos = advanceToEndOfTypeDesc();
281 break;
282 case '[': {
283 char elemTypeStart = firstCharacterOfArrayElementType();
284 if (elemTypeStart == 'T') {
285 endPos = parameterTypeDescs.indexOf(';', startPos);
286 currentPos = endPos;
287 } else if (elemTypeStart == 'L') {
288 endPos = advanceToEndOfTypeDesc();
289 } else {
290 endPos = currentPos + 1;
291 }
292 break;
293 }
294 default:
295 endPos = currentPos + 1;
296 break;
297 }
298
299 currentPos++;
300 String parameter = parameterTypeDescs.substring(startPos, endPos);
301 parameters.add(parameter);
302 }
303
304 private int advanceToEndOfTypeDesc() {
305 char c = '\0';
306
307 do {
308 currentPos++;
309 if (currentPos == lengthOfParameterTypeDescs) {
310 break;
311 }
312 c = parameterTypeDescs.charAt(currentPos);
313 } while (c != ';' && c != '<');
314
315 int endPos = currentPos;
316
317 if (c == '<') {
318 advancePastTypeArguments();
319 currentPos++;
320 }
321
322 return endPos;
323 }
324
325 private char firstCharacterOfArrayElementType() {
326 char c;
327
328 do {
329 currentPos++;
330 c = parameterTypeDescs.charAt(currentPos);
331 } while (c == '[');
332
333 return c;
334 }
335
336 private void advancePastTypeArguments() {
337 int angleBracketDepth = 1;
338
339 do {
340 currentPos++;
341 char c = parameterTypeDescs.charAt(currentPos);
342 if (c == '>') {
343 angleBracketDepth--;
344 } else if (c == '<') {
345 angleBracketDepth++;
346 }
347 } while (angleBracketDepth > 0);
348 }
349
350 public boolean satisfiesGenericSignature(@NonNull String otherSignature) {
351 GenericSignature other = new GenericSignature(otherSignature);
352 return areMatchingSignatures(other);
353 }
354
355 private boolean areMatchingSignatures(@NonNull GenericSignature other) {
356 int n = parameters.size();
357
358 if (n != other.parameters.size()) {
359 return false;
360 }
361
362 for (int i = 0; i < n; i++) {
363 String p1 = other.parameters.get(i);
364 String p2 = parameters.get(i);
365
366 if (!areParametersOfSameType(p1, p2)) {
367 return false;
368 }
369 }
370
371 return true;
372 }
373
374 @SuppressWarnings("MethodWithMultipleLoops")
375 private boolean areParametersOfSameType(@NonNull String param1, @NonNull String param2) {
376 if (param1.equals(param2)) {
377 return true;
378 }
379
380 int i = -1;
381 char c;
382 do {
383 i++;
384 c = param1.charAt(i);
385 } while (c == '[');
386 if (c != 'T') {
387 return false;
388 }
389
390 String typeVarName1 = param1.substring(i);
391 String typeVarName2 = param2.substring(i);
392 String typeArg1 = null;
393
394 for (Entry<String, String> typeParamAndArgName : typeParametersToTypeArgumentNames.entrySet()) {
395 String typeMappingKey = typeParamAndArgName.getKey();
396 String typeVarName = typeMappingKey.substring(typeMappingKey.indexOf(':') + 1);
397
398 if (typeVarName.equals(typeVarName1)) {
399 typeArg1 = typeParamAndArgName.getValue();
400 break;
401 }
402 }
403
404 return typeVarName2.equals(typeArg1);
405 }
406
407 public boolean satisfiesSignature(@NonNull String otherSignature) {
408 GenericSignature other = new GenericSignature(otherSignature);
409 return other.areMatchingSignatures(this);
410 }
411 }
412
413 @NonNull
414 public GenericSignature parseSignature(@NonNull String genericSignature) {
415 return new GenericSignature(genericSignature);
416 }
417
418 @NonNull
419 public String resolveSignature(@NonNull String ownerTypeDesc, @NonNull String genericSignature) {
420 addTypeArgumentsIfAvailable(ownerTypeDesc, genericSignature);
421
422 int p = genericSignature.lastIndexOf(')') + 1;
423 int q = genericSignature.length();
424 String returnType = genericSignature.substring(p, q);
425 String resolvedReturnType = replaceTypeParametersWithActualTypes(ownerTypeDesc, returnType);
426
427 StringBuilder finalSignature = new StringBuilder(genericSignature);
428 finalSignature.replace(p, q, resolvedReturnType);
429 return finalSignature.toString();
430 }
431
432 private void addTypeArgumentsIfAvailable(@NonNull String ownerTypeDesc, @NonNull String signature) {
433 int firstParen = signature.indexOf('(');
434 if (firstParen == 0) {
435 return;
436 }
437
438 int p = 1;
439 boolean lastMappingFound = false;
440
441 while (!lastMappingFound) {
442 int q = signature.indexOf(':', p);
443 String typeVar = signature.substring(p, q);
444
445 q++;
446
447 if (signature.charAt(q) == ':') {
448 q++;
449 }
450
451 int r = signature.indexOf(':', q);
452
453 if (r < 0) {
454 r = firstParen - 2;
455 lastMappingFound = true;
456 } else {
457 r = signature.lastIndexOf(';', r);
458 p = r + 1;
459 }
460
461 String typeArg = signature.substring(q, r);
462 addTypeMapping(ownerTypeDesc, typeVar, typeArg);
463 }
464 }
465
466 @NonNull
467 private String replaceTypeParametersWithActualTypes(@NonNull String ownerTypeDesc, @NonNull String typeDesc) {
468 if (typeDesc.charAt(0) == 'T' && !typeParametersToTypeArgumentNames.isEmpty()) {
469 return replaceTypeParameters(ownerTypeDesc, typeDesc);
470 }
471
472 int p = typeDesc.indexOf('<');
473
474 if (p < 0) {
475 return typeDesc;
476 }
477
478 String resolvedTypeDesc = typeDesc;
479
480 for (Entry<String, String> paramAndArg : typeParametersToTypeArgumentNames.entrySet()) {
481 String typeMappingKey = paramAndArg.getKey();
482 String typeParam = typeMappingKey.substring(typeMappingKey.indexOf(':') + 1) + ';';
483 String typeArg = paramAndArg.getValue() + ';';
484 resolvedTypeDesc = resolvedTypeDesc.replace(typeParam, typeArg);
485 }
486
487 return resolvedTypeDesc;
488 }
489
490 @NonNull
491 private String replaceTypeParameters(@NonNull String ownerTypeDesc, @NonNull String typeDesc) {
492 String typeParameter = typeDesc.substring(0, typeDesc.length() - 1);
493
494 while (true) {
495 @Nullable
496 String typeArg = typeParametersToTypeArgumentNames.get(ownerTypeDesc + ':' + typeParameter);
497
498 if (typeArg == null) {
499 return typeDesc;
500 }
501
502 if (typeArg.charAt(0) != 'T') {
503 return typeArg + ';';
504 }
505
506 typeParameter = typeArg;
507 }
508 }
509
510 @NonNull
511 public Type resolveTypeVariable(@NonNull TypeVariable<?> typeVariable) {
512 String typeVarKey = getTypeVariableKey(typeVariable);
513 @Nullable
514 Type typeArgument = typeParametersToTypeArguments.get(typeVarKey);
515
516 if (typeArgument == null) {
517 typeArgument = typeVariable.getBounds()[0];
518 }
519
520 if (typeArgument instanceof TypeVariable<?>) {
521 typeArgument = resolveTypeVariable((TypeVariable<?>) typeArgument);
522 }
523
524 return typeArgument;
525 }
526
527 @NonNull
528 private static String getTypeVariableKey(@NonNull TypeVariable<?> typeVariable) {
529 String ownerClassDesc = getOwnerClassDesc(typeVariable);
530 return ownerClassDesc + ':' + typeVariable.getName();
531 }
532
533 @NonNull
534 private static String getOwnerClassDesc(@NonNull TypeVariable<?> typeVariable) {
535 GenericDeclaration owner = typeVariable.getGenericDeclaration();
536 Class<?> ownerClass = owner instanceof Member ? ((Member) owner).getDeclaringClass() : (Class<?>) owner;
537 return getOwnerClassDesc(ownerClass);
538 }
539
540 public boolean areMatchingTypes(@NonNull Type declarationType, @NonNull Type realizationType) {
541 if (declarationType.equals(realizationType)) {
542 return true;
543 }
544
545 if (declarationType instanceof Class<?>) {
546 if (realizationType instanceof Class<?>) {
547 return ((Class<?>) declarationType).isAssignableFrom((Class<?>) realizationType);
548 }
549 } else if (declarationType instanceof TypeVariable<?>) {
550 if (realizationType instanceof TypeVariable<?>) {
551 return false;
552 }
553
554
555 if (areMatchingTypes((TypeVariable<?>) declarationType, realizationType)) {
556 return true;
557 }
558 } else if (declarationType instanceof ParameterizedType) {
559 ParameterizedType parameterizedDeclarationType = (ParameterizedType) declarationType;
560 ParameterizedType parameterizedRealizationType = getParameterizedType(realizationType);
561
562 if (parameterizedRealizationType != null) {
563 return areMatchingTypes(parameterizedDeclarationType, parameterizedRealizationType);
564 }
565 }
566
567 return false;
568 }
569
570 @Nullable
571 private static ParameterizedType getParameterizedType(@NonNull Type realizationType) {
572 if (realizationType instanceof ParameterizedType) {
573 return (ParameterizedType) realizationType;
574 }
575
576 if (realizationType instanceof Class<?>) {
577 return findRealizationSupertype((Class<?>) realizationType);
578 }
579
580 return null;
581 }
582
583 @Nullable
584 private static ParameterizedType findRealizationSupertype(@NonNull Class<?> realizationType) {
585 Type realizationSuperclass = realizationType.getGenericSuperclass();
586 ParameterizedType parameterizedRealizationType = null;
587
588 if (realizationSuperclass instanceof ParameterizedType) {
589 parameterizedRealizationType = (ParameterizedType) realizationSuperclass;
590 } else {
591 for (Type realizationSupertype : realizationType.getGenericInterfaces()) {
592 if (realizationSupertype instanceof ParameterizedType) {
593 parameterizedRealizationType = (ParameterizedType) realizationSupertype;
594 break;
595 }
596 }
597 }
598
599 return parameterizedRealizationType;
600 }
601
602 private boolean areMatchingTypes(@NonNull TypeVariable<?> declarationType, @NonNull Type realizationType) {
603 String typeVarKey = getTypeVariableKey(declarationType);
604 @Nullable
605 Type resolvedType = typeParametersToTypeArguments.get(typeVarKey);
606
607 return resolvedType != null && (resolvedType.equals(realizationType)
608 || typeSatisfiesResolvedTypeVariable(resolvedType, realizationType));
609 }
610
611 private boolean areMatchingTypes(@NonNull ParameterizedType declarationType,
612 @NonNull ParameterizedType realizationType) {
613 return declarationType.getRawType().equals(realizationType.getRawType())
614 && haveMatchingActualTypeArguments(declarationType, realizationType);
615 }
616
617 private boolean haveMatchingActualTypeArguments(@NonNull ParameterizedType declarationType,
618 @NonNull ParameterizedType realizationType) {
619 Type[] declaredTypeArguments = declarationType.getActualTypeArguments();
620 Type[] concreteTypeArguments = realizationType.getActualTypeArguments();
621
622 for (int i = 0, n = declaredTypeArguments.length; i < n; i++) {
623 Type declaredTypeArg = declaredTypeArguments[i];
624 Type concreteTypeArg = concreteTypeArguments[i];
625
626 if (declaredTypeArg instanceof TypeVariable<?>) {
627 if (areMatchingTypeArguments((TypeVariable<?>) declaredTypeArg, concreteTypeArg)) {
628 continue;
629 }
630 } else if (areMatchingTypes(declaredTypeArg, concreteTypeArg)) {
631 continue;
632 }
633
634 return false;
635 }
636
637 return true;
638 }
639
640 @SuppressWarnings("RedundantIfStatement")
641 private boolean areMatchingTypeArguments(@NonNull TypeVariable<?> declaredType, @NonNull Type concreteType) {
642 String typeVarKey = getTypeVariableKey(declaredType);
643 @Nullable
644 Type resolvedType = typeParametersToTypeArguments.get(typeVarKey);
645
646 if (resolvedType != null) {
647 if (resolvedType.equals(concreteType) || concreteType instanceof Class<?>
648 && typeSatisfiesResolvedTypeVariable(resolvedType, (Class<?>) concreteType)) {
649 return true;
650 }
651
652 if (concreteType instanceof WildcardType
653 && typeSatisfiesUpperBounds(resolvedType, ((WildcardType) concreteType).getUpperBounds())) {
654 return true;
655 }
656 } else if (typeSatisfiesUpperBounds(concreteType, declaredType.getBounds())) {
657 return true;
658 }
659
660 return false;
661 }
662
663 private boolean typeSatisfiesResolvedTypeVariable(@NonNull Type resolvedType, @NonNull Type realizationType) {
664 Class<?> realizationClass = getClassType(realizationType);
665 return typeSatisfiesResolvedTypeVariable(resolvedType, realizationClass);
666 }
667
668 private boolean typeSatisfiesResolvedTypeVariable(@NonNull Type resolvedType, @NonNull Class<?> realizationType) {
669 Class<?> resolvedClass = getClassType(resolvedType);
670 return resolvedClass.isAssignableFrom(realizationType);
671 }
672
673 private boolean typeSatisfiesUpperBounds(@NonNull Type type, @NonNull Type[] upperBounds) {
674 Class<?> classType = getClassType(type);
675
676 for (Type upperBound : upperBounds) {
677 Class<?> classBound = getClassType(upperBound);
678
679 if (!classBound.isAssignableFrom(classType)) {
680 return false;
681 }
682 }
683
684 return true;
685 }
686 }