001/* 002 * Copyright (C) 2012 Facebook, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may 005 * not use this file except in compliance with the License. You may obtain 006 * a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations 014 * under the License. 015 */ 016package com.facebook.swift.codec.metadata; 017 018import com.facebook.swift.codec.ThriftDocumentation; 019import com.facebook.swift.codec.ThriftOrder; 020import com.facebook.swift.codec.ThriftStruct; 021import com.facebook.swift.codec.ThriftUnion; 022import com.facebook.swift.codec.internal.coercion.DefaultJavaCoercions; 023import com.facebook.swift.codec.internal.coercion.FromThrift; 024import com.facebook.swift.codec.internal.coercion.ToThrift; 025import com.facebook.swift.codec.metadata.MetadataErrors.Monitor; 026import com.google.common.annotations.VisibleForTesting; 027import com.google.common.base.Function; 028import com.google.common.base.Joiner; 029import com.google.common.base.Preconditions; 030import com.google.common.collect.ImmutableList; 031import com.google.common.collect.Sets; 032import com.google.common.reflect.TypeToken; 033import com.google.common.util.concurrent.ListenableFuture; 034 035import javax.annotation.concurrent.ThreadSafe; 036 037import java.lang.reflect.Field; 038import java.lang.reflect.Method; 039import java.lang.reflect.Type; 040import java.nio.ByteBuffer; 041import java.util.ArrayDeque; 042import java.util.Deque; 043import java.util.HashMap; 044import java.util.Map; 045import java.util.Set; 046import java.util.concurrent.ConcurrentHashMap; 047import java.util.concurrent.ConcurrentMap; 048 049import static com.facebook.swift.codec.metadata.ReflectionHelper.getFutureReturnType; 050import static com.facebook.swift.codec.metadata.ReflectionHelper.getIterableType; 051import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapKeyType; 052import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapValueType; 053import static com.facebook.swift.codec.metadata.ThriftType.BINARY; 054import static com.facebook.swift.codec.metadata.ThriftType.BOOL; 055import static com.facebook.swift.codec.metadata.ThriftType.BYTE; 056import static com.facebook.swift.codec.metadata.ThriftType.DOUBLE; 057import static com.facebook.swift.codec.metadata.ThriftType.I16; 058import static com.facebook.swift.codec.metadata.ThriftType.I32; 059import static com.facebook.swift.codec.metadata.ThriftType.I64; 060import static com.facebook.swift.codec.metadata.ThriftType.STRING; 061import static com.facebook.swift.codec.metadata.ThriftType.VOID; 062import static com.facebook.swift.codec.metadata.ThriftType.array; 063import static com.facebook.swift.codec.metadata.ThriftType.enumType; 064import static com.facebook.swift.codec.metadata.ThriftType.list; 065import static com.facebook.swift.codec.metadata.ThriftType.map; 066import static com.facebook.swift.codec.metadata.ThriftType.set; 067import static com.facebook.swift.codec.metadata.ThriftType.struct; 068import static com.google.common.base.Preconditions.checkState; 069import static com.google.common.collect.Iterables.concat; 070import static com.google.common.collect.Iterables.transform; 071 072import static java.lang.reflect.Modifier.isStatic; 073 074/** 075 * ThriftCatalog contains the metadata for all known structs, enums and type coercions. Since, 076 * metadata extraction can be very expensive, and only single instance of the catalog should be 077 * created. 078 */ 079@ThreadSafe 080public class ThriftCatalog 081{ 082 private final MetadataErrors.Monitor monitor; 083 private final ConcurrentMap<Type, ThriftStructMetadata> structs = new ConcurrentHashMap<>(); 084 private final ConcurrentMap<Class<?>, ThriftEnumMetadata<?>> enums = new ConcurrentHashMap<>(); 085 private final ConcurrentMap<Type, TypeCoercion> coercions = new ConcurrentHashMap<>(); 086 private final ConcurrentMap<Class<?>, ThriftType> manualTypes = new ConcurrentHashMap<>(); 087 088 private final ThreadLocal<Deque<Type>> stack = new ThreadLocal<Deque<Type>>() 089 { 090 @Override 091 protected Deque<Type> initialValue() 092 { 093 return new ArrayDeque<>(); 094 } 095 }; 096 097 public ThriftCatalog() 098 { 099 this(MetadataErrors.NULL_MONITOR); 100 } 101 102 @VisibleForTesting 103 public ThriftCatalog(Monitor monitor) 104 { 105 this.monitor = monitor; 106 addDefaultCoercions(DefaultJavaCoercions.class); 107 } 108 109 @VisibleForTesting 110 Monitor getMonitor() 111 { 112 return monitor; 113 } 114 115 public void addThriftType(ThriftType thriftType) 116 { 117 manualTypes.put(TypeToken.of(thriftType.getJavaType()).getRawType(), thriftType); 118 } 119 120 /** 121 * Add the @ToThrift and @FromThrift coercions in the specified class to this catalog. All 122 * coercions must be symmetrical, so ever @ToThrift method must have a corresponding @FromThrift 123 * method. 124 */ 125 public void addDefaultCoercions(Class<?> coercionsClass) 126 { 127 Preconditions.checkNotNull(coercionsClass, "coercionsClass is null"); 128 Map<ThriftType, Method> toThriftCoercions = new HashMap<>(); 129 Map<ThriftType, Method> fromThriftCoercions = new HashMap<>(); 130 for (Method method : coercionsClass.getDeclaredMethods()) { 131 if (method.isAnnotationPresent(ToThrift.class)) { 132 verifyCoercionMethod(method); 133 ThriftType thriftType = getThriftType(method.getGenericReturnType()); 134 ThriftType coercedType = thriftType.coerceTo(method.getGenericParameterTypes()[0]); 135 136 Method oldValue = toThriftCoercions.put(coercedType, method); 137 Preconditions.checkArgument( 138 oldValue == null, 139 "Coercion class two @ToThrift methods (%s and %s) for type %s", 140 coercionsClass.getName(), 141 method, 142 oldValue, 143 coercedType); 144 } 145 else if (method.isAnnotationPresent(FromThrift.class)) { 146 verifyCoercionMethod(method); 147 ThriftType thriftType = getThriftType(method.getGenericParameterTypes()[0]); 148 ThriftType coercedType = thriftType.coerceTo(method.getGenericReturnType()); 149 150 Method oldValue = fromThriftCoercions.put(coercedType, method); 151 Preconditions.checkArgument( 152 oldValue == null, 153 "Coercion class two @FromThrift methods (%s and %s) for type %s", 154 coercionsClass.getName(), 155 method, 156 oldValue, 157 coercedType); 158 } 159 } 160 161 // assure coercions are symmetric 162 Set<ThriftType> difference = Sets.symmetricDifference(toThriftCoercions.keySet(), fromThriftCoercions.keySet()); 163 Preconditions.checkArgument( 164 difference.isEmpty(), 165 "Coercion class %s does not have matched @ToThrift and @FromThrift methods for types %s", 166 coercionsClass.getName(), 167 difference); 168 169 // add the coercions 170 Map<Type, TypeCoercion> coercions = new HashMap<>(); 171 for (Map.Entry<ThriftType, Method> entry : toThriftCoercions.entrySet()) { 172 ThriftType type = entry.getKey(); 173 Method toThriftMethod = entry.getValue(); 174 Method fromThriftMethod = fromThriftCoercions.get(type); 175 // this should never happen due to the difference check above, but be careful 176 Preconditions.checkState( 177 fromThriftMethod != null, 178 "Coercion class %s does not have matched @ToThrift and @FromThrift methods for type %s", 179 coercionsClass.getName(), 180 type); 181 TypeCoercion coercion = new TypeCoercion(type, toThriftMethod, fromThriftMethod); 182 coercions.put(type.getJavaType(), coercion); 183 } 184 this.coercions.putAll(coercions); 185 } 186 187 private void verifyCoercionMethod(Method method) 188 { 189 Preconditions.checkArgument(isStatic(method.getModifiers()), "Method %s is not static", method.toGenericString()); 190 Preconditions.checkArgument(method.getParameterTypes().length == 1, "Method %s must have exactly one parameter", method.toGenericString()); 191 Preconditions.checkArgument(method.getReturnType() != void.class, "Method %s must have a return value", method.toGenericString()); 192 } 193 194 /** 195 * Gets the default TypeCoercion (and associated ThriftType) for the specified Java type. 196 */ 197 public TypeCoercion getDefaultCoercion(Type type) 198 { 199 return coercions.get(type); 200 } 201 202 /** 203 * Gets the ThriftType for the specified Java type. The native Thrift type for the Java type will 204 * be inferred from the Java type, and if necessary type coercions will be applied. 205 * 206 * @return the ThriftType for the specified java type; never null 207 * @throws IllegalArgumentException if the Java Type can not be coerced to a ThriftType 208 */ 209 public ThriftType getThriftType(Type javaType) 210 throws IllegalArgumentException 211 { 212 Class<?> rawType = TypeToken.of(javaType).getRawType(); 213 ThriftType manualType = manualTypes.get(rawType); 214 if (manualType != null) { 215 return manualType; 216 } 217 if (boolean.class == rawType) { 218 return BOOL; 219 } 220 if (byte.class == rawType) { 221 return BYTE; 222 } 223 if (short.class == rawType) { 224 return I16; 225 } 226 if (int.class == rawType) { 227 return I32; 228 } 229 if (long.class == rawType) { 230 return I64; 231 } 232 if (double.class == rawType) { 233 return DOUBLE; 234 } 235 if (String.class == rawType) { 236 return STRING; 237 } 238 if (ByteBuffer.class.isAssignableFrom(rawType)) { 239 return BINARY; 240 } 241 if (Enum.class.isAssignableFrom(rawType)) { 242 Class<?> enumClass = TypeToken.of(javaType).getRawType(); 243 ThriftEnumMetadata<? extends Enum<?>> thriftEnumMetadata = getThriftEnumMetadata(enumClass); 244 return enumType(thriftEnumMetadata); 245 } 246 if (rawType.isArray()) { 247 Class<?> elementType = rawType.getComponentType(); 248 if (elementType == byte.class) { 249 // byte[] is encoded as BINARY and requires a coersion 250 return coercions.get(byte[].class).getThriftType(); 251 } 252 return array(getThriftType(elementType)); 253 } 254 if (Map.class.isAssignableFrom(rawType)) { 255 Type mapKeyType = getMapKeyType(javaType); 256 Type mapValueType = getMapValueType(javaType); 257 return map(getThriftType(mapKeyType), getThriftType(mapValueType)); 258 } 259 if (Set.class.isAssignableFrom(rawType)) { 260 Type elementType = getIterableType(javaType); 261 return set(getThriftType(elementType)); 262 } 263 if (Iterable.class.isAssignableFrom(rawType)) { 264 Type elementType = getIterableType(javaType); 265 return list(getThriftType(elementType)); 266 } 267 // The void type is used by service methods and is encoded as an empty struct 268 if (void.class.isAssignableFrom(rawType) || Void.class.isAssignableFrom(rawType)) { 269 return VOID; 270 } 271 if (rawType.isAnnotationPresent(ThriftStruct.class)) { 272 ThriftStructMetadata structMetadata = getThriftStructMetadata(javaType); 273 return struct(structMetadata); 274 } 275 if (rawType.isAnnotationPresent(ThriftUnion.class)) { 276 ThriftStructMetadata structMetadata = getThriftStructMetadata(javaType); 277 // An union looks like a struct with a single field. 278 return struct(structMetadata); 279 } 280 281 if (ListenableFuture.class.isAssignableFrom(rawType)) { 282 Type returnType = getFutureReturnType(javaType); 283 return getThriftType(returnType); 284 } 285 286 // coerce the type if possible 287 TypeCoercion coercion = coercions.get(javaType); 288 if (coercion != null) { 289 return coercion.getThriftType(); 290 } 291 throw new IllegalArgumentException("Type can not be coerced to a Thrift type: " + javaType); 292 } 293 294 public boolean isSupportedStructFieldType(Type javaType) 295 { 296 Class<?> rawType = TypeToken.of(javaType).getRawType(); 297 if (boolean.class == rawType) { 298 return true; 299 } 300 if (byte.class == rawType) { 301 return true; 302 } 303 if (short.class == rawType) { 304 return true; 305 } 306 if (int.class == rawType) { 307 return true; 308 } 309 if (long.class == rawType) { 310 return true; 311 } 312 if (double.class == rawType) { 313 return true; 314 } 315 if (String.class == rawType) { 316 return true; 317 } 318 if (ByteBuffer.class.isAssignableFrom(rawType)) { 319 return true; 320 } 321 if (Enum.class.isAssignableFrom(rawType)) { 322 return true; 323 } 324 if (rawType.isArray()) { 325 Class<?> elementType = rawType.getComponentType(); 326 return isSupportedArrayComponentType(elementType); 327 } 328 if (Map.class.isAssignableFrom(rawType)) { 329 Type mapKeyType = getMapKeyType(javaType); 330 Type mapValueType = getMapValueType(javaType); 331 return isSupportedStructFieldType(mapKeyType) && isSupportedStructFieldType(mapValueType); 332 } 333 if (Set.class.isAssignableFrom(rawType)) { 334 Type elementType = getIterableType(javaType); 335 return isSupportedStructFieldType(elementType); 336 } 337 if (Iterable.class.isAssignableFrom(rawType)) { 338 Type elementType = getIterableType(javaType); 339 return isSupportedStructFieldType(elementType); 340 } 341 if (rawType.isAnnotationPresent(ThriftStruct.class)) { 342 return true; 343 } 344 if (rawType.isAnnotationPresent(ThriftUnion.class)) { 345 return true; 346 } 347 348 // NOTE: void is not a supported struct type 349 350 // coerce the type if possible 351 TypeCoercion coercion = coercions.get(javaType); 352 if (coercion != null) { 353 return true; 354 } 355 return false; 356 } 357 358 public boolean isSupportedArrayComponentType(Class<?> componentType) 359 { 360 return boolean.class == componentType || 361 byte.class == componentType || 362 short.class == componentType || 363 int.class == componentType || 364 long.class == componentType || 365 double.class == componentType; 366 } 367 368 /** 369 * Gets the ThriftEnumMetadata for the specified enum class. If the enum class contains a method 370 * annotated with @ThriftEnumValue, the value of this method will be used for the encoded thrift 371 * value; otherwise the Enum.ordinal() method will be used. 372 */ 373 public <T extends Enum<T>> ThriftEnumMetadata<?> getThriftEnumMetadata(Class<?> enumClass) 374 { 375 ThriftEnumMetadata<?> enumMetadata = enums.get(enumClass); 376 if (enumMetadata == null) { 377 enumMetadata = new ThriftEnumMetadataBuilder<>((Class<T>) enumClass).build(); 378 379 ThriftEnumMetadata<?> current = enums.putIfAbsent(enumClass, enumMetadata); 380 if (current != null) { 381 enumMetadata = current; 382 } 383 } 384 return enumMetadata; 385 } 386 387 /** 388 * Gets the ThriftStructMetadata for the specified struct class. The struct class must be 389 * annotated with @ThriftStruct or @ThriftUnion. 390 */ 391 public <T> ThriftStructMetadata getThriftStructMetadata(Type structType) 392 { 393 ThriftStructMetadata structMetadata = structs.get(structType); 394 Class<?> structClass = TypeToken.of(structType).getRawType(); 395 if (structMetadata == null) { 396 if (structClass.isAnnotationPresent(ThriftStruct.class)) { 397 structMetadata = extractThriftStructMetadata(structType); 398 } 399 else if (structClass.isAnnotationPresent(ThriftUnion.class)) { 400 structMetadata = extractThriftUnionMetadata(structType); 401 } 402 else { 403 throw new IllegalStateException("getThriftStructMetadata called on a class that has no @ThriftStruct or @ThriftUnion annotation"); 404 } 405 406 ThriftStructMetadata current = structs.putIfAbsent(structType, structMetadata); 407 if (current != null) { 408 structMetadata = current; 409 } 410 } 411 return structMetadata; 412 } 413 414 415 private static Class<?> getSwiftMetaClassOf(Class<?> cls) throws ClassNotFoundException 416 { 417 ClassLoader loader = cls.getClassLoader(); 418 if (loader == null) { 419 throw new ClassNotFoundException("null class loader"); 420 } 421 return loader.loadClass(cls.getName() + "$swift_meta"); 422 } 423 424 @SuppressWarnings("PMD.EmptyCatchBlock") 425 public static ImmutableList<String> getThriftDocumentation(Class<?> objectClass) 426 { 427 ThriftDocumentation documentation = objectClass.getAnnotation(ThriftDocumentation.class); 428 429 if (documentation == null) { 430 try { 431 Class<?> swiftDocsClass = getSwiftMetaClassOf(objectClass); 432 433 documentation = swiftDocsClass.getAnnotation(ThriftDocumentation.class); 434 } 435 catch (ClassNotFoundException e) { 436 // ignored 437 } 438 } 439 440 return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value()); 441 } 442 443 @SuppressWarnings("PMD.EmptyCatchBlock") 444 public static ImmutableList<String> getThriftDocumentation(Method method) 445 { 446 ThriftDocumentation documentation = method.getAnnotation(ThriftDocumentation.class); 447 448 if (documentation == null) { 449 try { 450 Class<?> swiftDocsClass = getSwiftMetaClassOf(method.getDeclaringClass()); 451 452 documentation = swiftDocsClass.getDeclaredMethod(method.getName()).getAnnotation(ThriftDocumentation.class); 453 } 454 catch (ReflectiveOperationException e) { 455 // ignored 456 } 457 } 458 459 return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value()); 460 } 461 462 @SuppressWarnings("PMD.EmptyCatchBlock") 463 public static ImmutableList<String> getThriftDocumentation(Field field) 464 { 465 ThriftDocumentation documentation = field.getAnnotation(ThriftDocumentation.class); 466 467 if (documentation == null) { 468 try { 469 Class<?> swiftDocsClass = getSwiftMetaClassOf(field.getDeclaringClass()); 470 471 documentation = swiftDocsClass.getDeclaredField(field.getName()).getAnnotation(ThriftDocumentation.class); 472 } 473 catch (ReflectiveOperationException e) { 474 // ignored 475 } 476 } 477 478 return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value()); 479 } 480 481 @SuppressWarnings("PMD.EmptyCatchBlock") 482 public static <T extends Enum<T>> ImmutableList<String> getThriftDocumentation(Enum<T> enumConstant) 483 { 484 try { 485 Field f = enumConstant.getDeclaringClass().getField(enumConstant.name()); 486 return getThriftDocumentation(f); 487 } catch (ReflectiveOperationException e) { 488 // ignore 489 } 490 return ImmutableList.<String>of(); 491 } 492 493 @SuppressWarnings("PMD.EmptyCatchBlock") 494 public static Integer getMethodOrder(Method method) 495 { 496 ThriftOrder order = method.getAnnotation(ThriftOrder.class); 497 498 if (order == null) { 499 try { 500 Class<?> swiftDocsClass = getSwiftMetaClassOf(method.getDeclaringClass()); 501 502 order = swiftDocsClass.getDeclaredMethod(method.getName()).getAnnotation(ThriftOrder.class); 503 } 504 catch (ReflectiveOperationException e) { 505 // ignored 506 } 507 } 508 509 return order == null ? null : order.value(); 510 } 511 512 private ThriftStructMetadata extractThriftStructMetadata(Type structType) 513 { 514 Preconditions.checkNotNull(structType, "structType is null"); 515 516 Deque<Type> stack = this.stack.get(); 517 if (stack.contains(structType)) { 518 String path = Joiner.on("->").join(transform(concat(stack, ImmutableList.of(structType)), new Function<Type, Object>() 519 { 520 @Override 521 public Object apply(Type input) 522 { 523 return TypeToken.of(input).getRawType().getName(); 524 } 525 })); 526 throw new IllegalArgumentException("Circular references are not allowed: " + path); 527 } 528 529 stack.push(structType); 530 try { 531 ThriftStructMetadataBuilder builder = new ThriftStructMetadataBuilder(this, structType); 532 ThriftStructMetadata structMetadata = builder.build(); 533 return structMetadata; 534 } 535 finally { 536 Type top = stack.pop(); 537 checkState(structType.equals(top), 538 "ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s", 539 structType, 540 top); 541 } 542 } 543 544 private ThriftStructMetadata extractThriftUnionMetadata(Type unionType) 545 { 546 Preconditions.checkNotNull(unionType, "unionType is null"); 547 548 Deque<Type> stack = this.stack.get(); 549 if (stack.contains(unionType)) { 550 String path = Joiner.on("->").join(transform(concat(stack, ImmutableList.of(unionType)), new Function<Type, Object>() 551 { 552 @Override 553 public Object apply(Type input) 554 { 555 return TypeToken.of(input).getRawType().getName(); 556 } 557 })); 558 throw new IllegalArgumentException("Circular references are not allowed: " + path); 559 } 560 561 stack.push(unionType); 562 try { 563 ThriftUnionMetadataBuilder builder = new ThriftUnionMetadataBuilder(this, unionType); 564 ThriftStructMetadata unionMetadata = builder.build(); 565 return unionMetadata; 566 } 567 finally { 568 Type top = stack.pop(); 569 checkState(unionType.equals(top), 570 "ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s", 571 unionType, 572 top); 573 } 574 } 575 576}