1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.layoutlib.create; 18 19 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertSame; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 27 import com.android.tools.layoutlib.create.dataclass.ClassWithNative; 28 import com.android.tools.layoutlib.create.dataclass.OuterClass; 29 import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; 30 import com.android.tools.layoutlib.create.dataclass.OuterClass.StaticInnerClass; 31 32 import org.junit.Before; 33 import org.junit.Test; 34 import org.objectweb.asm.ClassReader; 35 import org.objectweb.asm.ClassVisitor; 36 import org.objectweb.asm.ClassWriter; 37 38 import java.io.IOException; 39 import java.io.PrintWriter; 40 import java.io.StringWriter; 41 import java.lang.annotation.Annotation; 42 import java.lang.reflect.Constructor; 43 import java.lang.reflect.InvocationTargetException; 44 import java.lang.reflect.Method; 45 import java.lang.reflect.Modifier; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.Map; 49 import java.util.Map.Entry; 50 import java.util.Set; 51 52 public class DelegateClassAdapterTest { 53 54 private MockLog mLog; 55 56 private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getName(); 57 private static final String OUTER_CLASS_NAME = OuterClass.class.getName(); 58 private static final String INNER_CLASS_NAME = InnerClass.class.getName(); 59 private static final String STATIC_INNER_CLASS_NAME = StaticInnerClass.class.getName(); 60 61 @Before setUp()62 public void setUp() throws Exception { 63 mLog = new MockLog(); 64 mLog.setVerbose(true); // capture debug error too 65 } 66 67 /** 68 * Tests that a class not being modified still works. 69 */ 70 @Test testNoOp()71 public void testNoOp() throws Throwable { 72 // create an instance of the class that will be modified 73 // (load the class in a distinct class loader so that we can trash its definition later) 74 ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { }; 75 @SuppressWarnings("unchecked") 76 Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME); 77 ClassWithNative instance1 = clazz1.newInstance(); 78 assertEquals(42, instance1.add(20, 22)); 79 try { 80 instance1.callNativeInstance(10, 3.1415, new Object[0] ); 81 fail("Test should have failed to invoke callTheNativeMethod [1]"); 82 } catch (UnsatisfiedLinkError e) { 83 // This is expected to fail since the native method is not implemented. 84 } 85 86 // Now process it but tell the delegate to not modify any method 87 ClassWriter cw = new ClassWriter(0 /*flags*/); 88 89 HashSet<String> delegateMethods = new HashSet<>(); 90 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); 91 DelegateClassAdapter cv = new DelegateClassAdapter( 92 mLog, cw, internalClassName, delegateMethods); 93 94 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); 95 cr.accept(cv, 0 /* flags */); 96 97 // Load the generated class in a different class loader and try it again 98 99 ClassLoader2 cl2 = null; 100 try { 101 cl2 = new ClassLoader2() { 102 @Override 103 public void testModifiedInstance() throws Exception { 104 Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME); 105 Object i2 = clazz2.newInstance(); 106 assertNotNull(i2); 107 assertEquals(42, callAdd(i2, 20, 22)); 108 109 try { 110 callCallNativeInstance(i2, 10, 3.1415, new Object[0]); 111 fail("Test should have failed to invoke callTheNativeMethod [2]"); 112 } catch (InvocationTargetException e) { 113 // This is expected to fail since the native method has NOT been 114 // overridden here. 115 assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass()); 116 } 117 118 // Check that the native method does NOT have the new annotation 119 Method[] m = clazz2.getDeclaredMethods(); 120 Method nativeInstanceMethod = null; 121 for (Method method : m) { 122 if ("native_instance".equals(method.getName())) { 123 nativeInstanceMethod = method; 124 break; 125 } 126 } 127 assertNotNull(nativeInstanceMethod); 128 assertTrue(Modifier.isNative(nativeInstanceMethod.getModifiers())); 129 Annotation[] a = nativeInstanceMethod.getAnnotations(); 130 assertEquals(0, a.length); 131 } 132 }; 133 cl2.add(NATIVE_CLASS_NAME, cw); 134 cl2.testModifiedInstance(); 135 } catch (Throwable t) { 136 throw dumpGeneratedClass(t, cl2); 137 } 138 } 139 140 @Test testConstructorAfterDelegate()141 public void testConstructorAfterDelegate() throws Throwable { 142 ClassWriter cw = new ClassWriter(0 /*flags*/); 143 144 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); 145 146 HashSet<String> delegateMethods = new HashSet<>(); 147 delegateMethods.add("<init>"); 148 DelegateClassAdapter cv = new DelegateClassAdapter( 149 mLog, cw, internalClassName, delegateMethods); 150 151 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); 152 cr.accept(cv, 0 /* flags */); 153 154 ClassLoader2 cl2 = null; 155 try { 156 cl2 = new ClassLoader2() { 157 @Override 158 public void testModifiedInstance() throws Exception { 159 Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME); 160 Object i2 = clazz2.newInstance(); 161 assertNotNull(i2); 162 assertEquals(123, clazz2.getField("mId").getInt(i2)); 163 } 164 }; 165 cl2.add(NATIVE_CLASS_NAME, cw); 166 cl2.testModifiedInstance(); 167 } catch (Throwable t) { 168 throw dumpGeneratedClass(t, cl2); 169 } 170 } 171 172 @Test testInnerConstructorAfterDelegate()173 public void testInnerConstructorAfterDelegate() throws Throwable { 174 ClassWriter cw = new ClassWriter(0 /*flags*/); 175 176 String internalClassName = INNER_CLASS_NAME.replace('.', '/'); 177 178 HashSet<String> delegateMethods = new HashSet<>(); 179 delegateMethods.add("<init>"); 180 DelegateClassAdapter cv = new DelegateClassAdapter( 181 mLog, cw, internalClassName, delegateMethods); 182 183 ClassReader cr = new ClassReader(INNER_CLASS_NAME); 184 cr.accept(cv, 0 /* flags */); 185 186 ClassLoader2 cl2 = null; 187 try { 188 cl2 = new ClassLoader2() { 189 @Override 190 public void testModifiedInstance() throws Exception { 191 Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); 192 Object o2 = outerClazz2.newInstance(); 193 194 Class<?> clazz2 = loadClass(INNER_CLASS_NAME); 195 Object i2 = clazz2.getConstructor(outerClazz2).newInstance(o2); 196 assertNotNull(i2); 197 assertEquals(98, clazz2.getField("mInnerId").getInt(i2)); 198 } 199 }; 200 cl2.add(INNER_CLASS_NAME, cw); 201 cl2.testModifiedInstance(); 202 } catch (Throwable t) { 203 throw dumpGeneratedClass(t, cl2); 204 } 205 } 206 207 @Test testStaticInnerConstructorAfterDelegate()208 public void testStaticInnerConstructorAfterDelegate() throws Throwable { 209 ClassWriter cw = new ClassWriter(0 /*flags*/); 210 211 String internalClassName = STATIC_INNER_CLASS_NAME.replace('.', '/'); 212 213 HashSet<String> delegateMethods = new HashSet<>(); 214 delegateMethods.add("<init>"); 215 DelegateClassAdapter cv = new DelegateClassAdapter( 216 mLog, cw, internalClassName, delegateMethods); 217 218 ClassReader cr = new ClassReader(STATIC_INNER_CLASS_NAME); 219 cr.accept(cv, 0 /* flags */); 220 221 ClassLoader2 cl2 = null; 222 try { 223 cl2 = new ClassLoader2() { 224 @Override 225 public void testModifiedInstance() throws Exception { 226 Class<?> clazz2 = loadClass(STATIC_INNER_CLASS_NAME); 227 Object i2 = clazz2.newInstance(); 228 assertNotNull(i2); 229 assertEquals(42, clazz2.getField("mStaticInnerId").getInt(i2)); 230 } 231 }; 232 cl2.add(STATIC_INNER_CLASS_NAME, cw); 233 cl2.testModifiedInstance(); 234 } catch (Throwable t) { 235 throw dumpGeneratedClass(t, cl2); 236 } 237 } 238 239 @Test testDelegateNative()240 public void testDelegateNative() throws Throwable { 241 ClassWriter cw = new ClassWriter(0 /*flags*/); 242 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); 243 244 HashSet<String> delegateMethods = new HashSet<>(); 245 delegateMethods.add(DelegateClassAdapter.ALL_NATIVES); 246 DelegateClassAdapter cv = new DelegateClassAdapter( 247 mLog, cw, internalClassName, delegateMethods); 248 249 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); 250 cr.accept(cv, 0 /* flags */); 251 252 // Load the generated class in a different class loader and try it 253 ClassLoader2 cl2 = null; 254 try { 255 cl2 = new ClassLoader2() { 256 @Override 257 public void testModifiedInstance() throws Exception { 258 Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME); 259 Object i2 = clazz2.newInstance(); 260 assertNotNull(i2); 261 262 // Use reflection to access inner methods 263 assertEquals(42, callAdd(i2, 20, 22)); 264 265 Object[] objResult = new Object[] { null }; 266 int result = callCallNativeInstance(i2, 10, 3.1415, objResult); 267 assertEquals((int)(10 + 3.1415), result); 268 assertSame(i2, objResult[0]); 269 270 // Check that the native method now has the new annotation and is not native 271 Method[] m = clazz2.getDeclaredMethods(); 272 Method nativeInstanceMethod = null; 273 for (Method method : m) { 274 if ("native_instance".equals(method.getName())) { 275 nativeInstanceMethod = method; 276 break; 277 } 278 } 279 assertNotNull(nativeInstanceMethod); 280 assertFalse(Modifier.isNative(nativeInstanceMethod.getModifiers())); 281 Annotation[] a = nativeInstanceMethod.getAnnotations(); 282 assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName()); 283 } 284 }; 285 cl2.add(NATIVE_CLASS_NAME, cw); 286 cl2.testModifiedInstance(); 287 } catch (Throwable t) { 288 throw dumpGeneratedClass(t, cl2); 289 } 290 } 291 292 @Test testDelegateInner()293 public void testDelegateInner() throws Throwable { 294 // We'll delegate the "get" method of both the inner and outer class. 295 HashSet<String> delegateMethods = new HashSet<>(); 296 delegateMethods.add("get"); 297 delegateMethods.add("privateMethod"); 298 299 // Generate the delegate for the outer class. 300 ClassWriter cwOuter = new ClassWriter(0 /*flags*/); 301 String outerClassName = OUTER_CLASS_NAME.replace('.', '/'); 302 DelegateClassAdapter cvOuter = new DelegateClassAdapter( 303 mLog, cwOuter, outerClassName, delegateMethods); 304 ClassReader cr = new ClassReader(OUTER_CLASS_NAME); 305 cr.accept(cvOuter, 0 /* flags */); 306 307 // Generate the delegate for the inner class. 308 ClassWriter cwInner = new ClassWriter(0 /*flags*/); 309 String innerClassName = INNER_CLASS_NAME.replace('.', '/'); 310 DelegateClassAdapter cvInner = new DelegateClassAdapter( 311 mLog, cwInner, innerClassName, delegateMethods); 312 cr = new ClassReader(INNER_CLASS_NAME); 313 cr.accept(cvInner, 0 /* flags */); 314 315 // Load the generated classes in a different class loader and try them 316 ClassLoader2 cl2 = null; 317 try { 318 cl2 = new ClassLoader2() { 319 @Override 320 public void testModifiedInstance() throws Exception { 321 322 // Check the outer class 323 Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); 324 Object o2 = outerClazz2.newInstance(); 325 assertNotNull(o2); 326 327 // The original Outer.get returns 1+10+20, 328 // but the delegate makes it return 4+10+20 329 assertEquals(4+10+20, callGet(o2, 10, 20)); 330 assertEquals(1+10+20, callGet_Original(o2, 10, 20)); 331 332 // The original Outer has a private method, 333 // so by default we can't access it. 334 boolean gotIllegalAccessException = false; 335 try { 336 callMethod(o2, "privateMethod", false /*makePublic*/); 337 } catch(IllegalAccessException e) { 338 gotIllegalAccessException = true; 339 } 340 assertTrue(gotIllegalAccessException); 341 342 // The private method from original Outer has been 343 // delegated. The delegate generated should have the 344 // same access. 345 gotIllegalAccessException = false; 346 try { 347 assertEquals("outerPrivateMethod", 348 callMethod(o2, "privateMethod_Original", false /*makePublic*/)); 349 } catch (IllegalAccessException e) { 350 gotIllegalAccessException = true; 351 } 352 assertTrue(gotIllegalAccessException); 353 354 // Check the inner class. Since it's not a static inner class, we need 355 // to use the hidden constructor that takes the outer class as first parameter. 356 Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME); 357 Constructor<?> innerCons = innerClazz2.getConstructor(outerClazz2); 358 Object i2 = innerCons.newInstance(o2); 359 assertNotNull(i2); 360 361 // The original Inner.get returns 3+10+20, 362 // but the delegate makes it return 6+10+20 363 assertEquals(6+10+20, callGet(i2, 10, 20)); 364 assertEquals(3+10+20, callGet_Original(i2, 10, 20)); 365 } 366 }; 367 cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray()); 368 cl2.add(INNER_CLASS_NAME, cwInner.toByteArray()); 369 cl2.testModifiedInstance(); 370 } catch (Throwable t) { 371 throw dumpGeneratedClass(t, cl2); 372 } 373 } 374 375 @Test testDelegateStaticInner()376 public void testDelegateStaticInner() throws Throwable { 377 // We'll delegate the "get" method of both the inner and outer class. 378 HashSet<String> delegateMethods = new HashSet<>(); 379 delegateMethods.add("get"); 380 381 // Generate the delegate for the outer class. 382 ClassWriter cwOuter = new ClassWriter(0 /*flags*/); 383 String outerClassName = OUTER_CLASS_NAME.replace('.', '/'); 384 DelegateClassAdapter cvOuter = new DelegateClassAdapter( 385 mLog, cwOuter, outerClassName, delegateMethods); 386 ClassReader cr = new ClassReader(OUTER_CLASS_NAME); 387 cr.accept(cvOuter, 0 /* flags */); 388 389 // Generate the delegate for the static inner class. 390 ClassWriter cwInner = new ClassWriter(0 /*flags*/); 391 String innerClassName = STATIC_INNER_CLASS_NAME.replace('.', '/'); 392 DelegateClassAdapter cvInner = new DelegateClassAdapter( 393 mLog, cwInner, innerClassName, delegateMethods); 394 cr = new ClassReader(STATIC_INNER_CLASS_NAME); 395 cr.accept(cvInner, 0 /* flags */); 396 397 // Load the generated classes in a different class loader and try them 398 ClassLoader2 cl2 = null; 399 try { 400 cl2 = new ClassLoader2() { 401 @Override 402 public void testModifiedInstance() throws Exception { 403 404 // Check the outer class 405 Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); 406 Object o2 = outerClazz2.newInstance(); 407 assertNotNull(o2); 408 409 // Check the inner class. Since it's not a static inner class, we need 410 // to use the hidden constructor that takes the outer class as first parameter. 411 Class<?> innerClazz2 = loadClass(STATIC_INNER_CLASS_NAME); 412 Constructor<?> innerCons = innerClazz2.getConstructor(); 413 Object i2 = innerCons.newInstance(); 414 assertNotNull(i2); 415 416 // The original StaticInner.get returns 100+10+20, 417 // but the delegate makes it return 6+10+20 418 assertEquals(6+10+20, callGet(i2, 10, 20)); 419 assertEquals(100+10+20, callGet_Original(i2, 10, 20)); 420 } 421 }; 422 cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray()); 423 cl2.add(STATIC_INNER_CLASS_NAME, cwInner.toByteArray()); 424 cl2.testModifiedInstance(); 425 } catch (Throwable t) { 426 throw dumpGeneratedClass(t, cl2); 427 } 428 } 429 430 //------- 431 432 /** 433 * A class loader than can define and instantiate our modified classes. 434 * <p/> 435 * The trick here is that this class loader will test our <em>modified</em> version 436 * of the classes, the one with the delegate calls. 437 * <p/> 438 * Trying to do so in the original class loader generates all sort of link issues because 439 * there are 2 different definitions of the same class name. This class loader will 440 * define and load the class when requested by name and provide helpers to access the 441 * instance methods via reflection. 442 */ 443 private abstract class ClassLoader2 extends ClassLoader { 444 445 private final Map<String, byte[]> mClassDefs = new HashMap<>(); 446 ClassLoader2()447 public ClassLoader2() { 448 super(null); 449 } 450 add(String className, byte[] definition)451 public ClassLoader2 add(String className, byte[] definition) { 452 mClassDefs.put(className, definition); 453 return this; 454 } 455 add(String className, ClassWriter rewrittenClass)456 public ClassLoader2 add(String className, ClassWriter rewrittenClass) { 457 mClassDefs.put(className, rewrittenClass.toByteArray()); 458 return this; 459 } 460 getByteCode()461 private Set<Entry<String, byte[]>> getByteCode() { 462 return mClassDefs.entrySet(); 463 } 464 465 @SuppressWarnings("unused") 466 @Override findClass(String name)467 protected Class<?> findClass(String name) throws ClassNotFoundException { 468 try { 469 return super.findClass(name); 470 } catch (ClassNotFoundException e) { 471 472 byte[] def = mClassDefs.get(name); 473 if (def != null) { 474 // Load the modified ClassWithNative from its bytes representation. 475 return defineClass(name, def, 0, def.length); 476 } 477 478 try { 479 // Load everything else from the original definition into the new class loader. 480 ClassReader cr = new ClassReader(name); 481 ClassWriter cw = new ClassWriter(0); 482 cr.accept(cw, 0); 483 byte[] bytes = cw.toByteArray(); 484 return defineClass(name, bytes, 0, bytes.length); 485 486 } catch (IOException ioe) { 487 throw new RuntimeException(ioe); 488 } 489 } 490 } 491 492 /** 493 * Accesses {@link OuterClass#get} or {@link InnerClass#get}via reflection. 494 */ callGet(Object instance, int a, long b)495 public int callGet(Object instance, int a, long b) throws Exception { 496 Method m = instance.getClass().getMethod("get", 497 int.class, long.class); 498 499 Object result = m.invoke(instance, a, b); 500 return (Integer) result; 501 } 502 503 /** 504 * Accesses the "_Original" methods for {@link OuterClass#get} 505 * or {@link InnerClass#get}via reflection. 506 */ callGet_Original(Object instance, int a, long b)507 public int callGet_Original(Object instance, int a, long b) throws Exception { 508 Method m = instance.getClass().getMethod("get_Original", 509 int.class, long.class); 510 511 Object result = m.invoke(instance, a, b); 512 return (Integer) result; 513 } 514 515 /** 516 * Accesses the any declared method that takes no parameter via reflection. 517 */ 518 @SuppressWarnings("unchecked") callMethod(Object instance, String methodName, boolean makePublic)519 public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception { 520 Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null); 521 522 boolean wasAccessible = m.isAccessible(); 523 if (makePublic && !wasAccessible) { 524 m.setAccessible(true); 525 } 526 527 Object result = m.invoke(instance, (Object[])null); 528 529 if (makePublic && !wasAccessible) { 530 m.setAccessible(false); 531 } 532 533 return (T) result; 534 } 535 536 /** 537 * Accesses {@link ClassWithNative#add(int, int)} via reflection. 538 */ callAdd(Object instance, int a, int b)539 public int callAdd(Object instance, int a, int b) throws Exception { 540 Method m = instance.getClass().getMethod("add", 541 int.class, int.class); 542 543 Object result = m.invoke(instance, a, b); 544 return (Integer) result; 545 } 546 547 /** 548 * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])} 549 * via reflection. 550 */ callCallNativeInstance(Object instance, int a, double d, Object[] o)551 public int callCallNativeInstance(Object instance, int a, double d, Object[] o) 552 throws Exception { 553 Method m = instance.getClass().getMethod("callNativeInstance", 554 int.class, double.class, Object[].class); 555 556 Object result = m.invoke(instance, a, d, o); 557 return (Integer) result; 558 } 559 testModifiedInstance()560 public abstract void testModifiedInstance() throws Exception; 561 } 562 563 /** 564 * For debugging, it's useful to dump the content of the generated classes 565 * along with the exception that was generated. 566 * 567 * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor 568 * class and associated utilities which are found in the ASM source jar. Since we don't 569 * want that dependency in the source code, we only put it manually for development and 570 * access the TraceClassVisitor via reflection if present. 571 * 572 * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()} 573 * @param cl2 The {@link ClassLoader2} instance with the generated bytecode. 574 * @return Either original {@code t} or a new wrapper {@link Throwable} 575 */ dumpGeneratedClass(Throwable t, ClassLoader2 cl2)576 private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) { 577 try { 578 // For debugging, dump the bytecode of the class in case of unexpected error 579 // if we can find the TraceClassVisitor class. 580 Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor"); 581 582 StringBuilder sb = new StringBuilder(); 583 sb.append('\n').append(t.getClass().getCanonicalName()); 584 if (t.getMessage() != null) { 585 sb.append(": ").append(t.getMessage()); 586 } 587 588 for (Entry<String, byte[]> entry : cl2.getByteCode()) { 589 String className = entry.getKey(); 590 byte[] bytes = entry.getValue(); 591 592 StringWriter sw = new StringWriter(); 593 PrintWriter pw = new PrintWriter(sw); 594 // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw); 595 Constructor<?> cons = tcvClass.getConstructor(pw.getClass()); 596 Object tcv = cons.newInstance(pw); 597 ClassReader cr2 = new ClassReader(bytes); 598 cr2.accept((ClassVisitor) tcv, 0 /* flags */); 599 600 sb.append("\nBytecode dump: <").append(className).append(">:\n") 601 .append(sw.toString()); 602 } 603 604 // Re-throw exception with new message 605 return new RuntimeException(sb.toString(), t); 606 } catch (Throwable ignore) { 607 // In case of problem, just throw the original exception as-is. 608 return t; 609 } 610 } 611 612 } 613