001package io.ebean.enhance.entity; 002 003import io.ebean.enhance.asm.ClassVisitor; 004import io.ebean.enhance.asm.Label; 005import io.ebean.enhance.asm.MethodVisitor; 006import io.ebean.enhance.asm.Opcodes; 007import io.ebean.enhance.asm.Type; 008import io.ebean.enhance.common.ClassMeta; 009import io.ebean.enhance.common.EnhanceConstants; 010import io.ebean.enhance.common.VisitUtil; 011 012import java.util.HashSet; 013 014/** 015 * Holds meta data for a field. 016 * <p> 017 * This can then generate the appropriate byte code for this field. 018 * </p> 019 */ 020public class FieldMeta implements Opcodes, EnhanceConstants { 021 022 private final ClassMeta classMeta; 023 private final String fieldClass; 024 private final String fieldName; 025 private final String fieldDesc; 026 027 private final HashSet<String> annotations = new HashSet<>(); 028 029 private final Type asmType; 030 031 private final boolean primitiveType; 032 private final boolean objectType; 033 034 private final String getMethodName; 035 private final String getMethodDesc; 036 private final String setMethodName; 037 private final String setMethodDesc; 038 private final String getNoInterceptMethodName; 039 private final String setNoInterceptMethodName; 040 041 private int indexPosition; 042 043 /** 044 * Construct based on field name and desc from reading byte code. 045 * <p> 046 * Used for reading local fields (not inherited) via visiting the class bytes. 047 * </p> 048 */ 049 public FieldMeta(ClassMeta classMeta, String name, String desc, String fieldClass) { 050 this.classMeta = classMeta; 051 this.fieldName = name; 052 this.fieldDesc = desc; 053 this.fieldClass = fieldClass; 054 this.asmType = Type.getType(desc); 055 056 int sort = asmType.getSort(); 057 this.primitiveType = sort > Type.VOID && sort <= Type.DOUBLE; 058 this.objectType = sort == Type.OBJECT; 059 this.getMethodDesc = "()" + desc; 060 this.setMethodDesc = "(" + desc + ")V"; 061 this.getMethodName = "_ebean_get_" + name; 062 this.setMethodName = "_ebean_set_" + name; 063 this.getNoInterceptMethodName = "_ebean_getni_" + name; 064 this.setNoInterceptMethodName = "_ebean_setni_" + name; 065 } 066 067 public void setIndexPosition(int indexPosition) { 068 this.indexPosition = indexPosition; 069 } 070 071 @Override 072 public String toString() { 073 return fieldName; 074 } 075 076 /** 077 * Return the field name. 078 */ 079 public String getFieldName() { 080 return fieldName; 081 } 082 083 /** 084 * Return true if this is a primitiveType. 085 */ 086 public boolean isPrimitiveType() { 087 return primitiveType; 088 } 089 090 /** 091 * Add a field annotation. 092 */ 093 void addAnnotationDesc(String desc) { 094 annotations.add(desc); 095 } 096 097 /** 098 * Return the field name. 099 */ 100 public String getName() { 101 return fieldName; 102 } 103 104 private boolean isInterceptGet() { 105 return !isId() && !isTransient(); 106 } 107 108 private boolean isInterceptSet() { 109 return !isId() && !isTransient() && !isToMany(); 110 } 111 112 /** 113 * Return true if this field type is an Array of Objects. 114 * <p> 115 * We can not support Object Arrays for field types. 116 * </p> 117 */ 118 public boolean isObjectArray() { 119 if (fieldDesc.charAt(0) == '[') { 120 if (fieldDesc.length() > 2) { 121 if (!isTransient()) { 122 System.err.println("ERROR: We can not support Object Arrays... for field: " + fieldName); 123 } 124 return true; 125 } 126 } 127 return false; 128 } 129 130 /** 131 * Return true is this is a persistent field. 132 */ 133 public boolean isPersistent() { 134 return !isTransient(); 135 } 136 137 /** 138 * Return true if this is a transient field. 139 */ 140 public boolean isTransient() { 141 return annotations.contains("Ljavax/persistence/Transient;") 142 || annotations.contains(L_DRAFT) 143 || fieldName.equals(MOCKITO_INTERCEPTOR); 144 } 145 146 /** 147 * Return true if this is an ID field. 148 * <p> 149 * ID fields are used in generating equals() logic based on identity. 150 * </p> 151 */ 152 public boolean isId() { 153 return (annotations.contains("Ljavax/persistence/Id;") 154 || annotations.contains("Ljavax/persistence/EmbeddedId;")); 155 } 156 157 /** 158 * Return true if this is a OneToMany or ManyToMany field. 159 */ 160 public boolean isToMany() { 161 return annotations.contains("Ljavax/persistence/OneToMany;") 162 || annotations.contains("Ljavax/persistence/ManyToMany;"); 163 } 164 165 private boolean isManyToMany() { 166 return annotations.contains("Ljavax/persistence/ManyToMany;"); 167 } 168 169 /** 170 * Control initialisation of ToMany and DbArray collection properties. 171 * This means these properties are lazy initialised on demand. 172 */ 173 public boolean isInitMany() { 174 return isToMany() || isDbArray(); 175 } 176 177 private boolean isDbArray() { 178 return annotations.contains("Lio/ebean/annotation/DbArray;"); 179 } 180 181 /** 182 * Return true if this is an Embedded field. 183 */ 184 boolean isEmbedded() { 185 return annotations.contains("Ljavax/persistence/Embedded;"); 186 } 187 188 /** 189 * Return true if the field is local to this class. Returns false if the field 190 * is actually on a super class. 191 */ 192 boolean isLocalField(ClassMeta classMeta) { 193 return fieldClass.equals(classMeta.getClassName()); 194 } 195 196 /** 197 * Append byte code to return the Id value (for primitives). 198 */ 199 void appendGetPrimitiveIdValue(MethodVisitor mv, ClassMeta classMeta) { 200 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false); 201 } 202 203 /** 204 * Append compare instructions if its a long, float or double. 205 */ 206 void appendCompare(MethodVisitor mv, ClassMeta classMeta) { 207 if (primitiveType) { 208 if (classMeta.isLog(4)) { 209 classMeta.log(" ... getIdentity compare primitive field[" + fieldName + "] type[" + fieldDesc + "]"); 210 } 211 if (fieldDesc.equals("J")) { 212 // long compare to 0 213 mv.visitInsn(LCONST_0); 214 mv.visitInsn(LCMP); 215 216 } else if (fieldDesc.equals("D")) { 217 // double compare to 0 218 mv.visitInsn(DCONST_0); 219 mv.visitInsn(DCMPL); 220 221 } else if (fieldDesc.equals("F")) { 222 // float compare to 0 223 mv.visitInsn(FCONST_0); 224 mv.visitInsn(FCMPL); 225 } 226 // no extra instructions required for 227 // int, short, byte, char 228 } 229 } 230 231 /** 232 * Append code to get the Object value of a primitive. 233 * <p> 234 * This becomes a Integer.valueOf(someInt); or similar. 235 * </p> 236 */ 237 void appendValueOf(MethodVisitor mv) { 238 if (primitiveType) { 239 // use valueOf methods to return primitives as objects 240 Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType); 241 String objDesc = objectWrapperType.getInternalName(); 242 String primDesc = asmType.getDescriptor(); 243 mv.visitMethodInsn(Opcodes.INVOKESTATIC, objDesc, "valueOf", "(" + primDesc + ")L" + objDesc + ";", false); 244 } 245 } 246 247 /** 248 * As part of the switch statement to read the fields generate the get code. 249 */ 250 void appendSwitchGet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) { 251 if (intercept) { 252 // use the special get method with interception... 253 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false); 254 } else { 255 if (isLocalField(classMeta)) { 256 mv.visitFieldInsn(GETFIELD, classMeta.getClassName(), fieldName, fieldDesc); 257 } else { 258 // field is on a superclass... so use virtual getNoInterceptMethodName 259 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getNoInterceptMethodName, getMethodDesc, false); 260 } 261 } 262 if (primitiveType) { 263 appendValueOf(mv); 264 } 265 } 266 267 void appendSwitchSet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) { 268 if (primitiveType) { 269 // convert Object to primitive first... 270 Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType); 271 String primDesc = asmType.getDescriptor(); 272 String primType = asmType.getClassName(); 273 String objInt = objectWrapperType.getInternalName(); 274 mv.visitTypeInsn(CHECKCAST, objInt); 275 mv.visitMethodInsn(INVOKEVIRTUAL, objInt, primType + "Value", "()" + primDesc, false); 276 } else { 277 // check correct object type 278 mv.visitTypeInsn(CHECKCAST, asmType.getInternalName()); 279 } 280 281 if (intercept) { 282 // go through the set method to check for interception... 283 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setMethodName, setMethodDesc, false); 284 } else { 285 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setNoInterceptMethodName, setMethodDesc, false); 286 } 287 } 288 289 /** 290 * Add get and set methods for field access/interception. 291 */ 292 public void addGetSetMethods(ClassVisitor cv, ClassMeta classMeta) { 293 if (!isLocalField(classMeta)) { 294 String msg = "ERROR: " + fieldClass + " != " + classMeta.getClassName() + " for field " 295 + fieldName + " " + fieldDesc; 296 throw new RuntimeException(msg); 297 } 298 // add intercepting methods that are used to replace the 299 // standard GETFIELD PUTFIELD byte codes for field access 300 addGet(cv, classMeta); 301 addSet(cv, classMeta); 302 303 // add non-interception methods... so that we can get access 304 // to private fields on super classes 305 addGetNoIntercept(cv, classMeta); 306 addSetNoIntercept(cv, classMeta); 307 } 308 309 private String getInitCollectionClass() { 310 final boolean dbArray = isDbArray(); 311 if (fieldDesc.equals("Ljava/util/List;")) { 312 return dbArray ? ARRAYLIST : BEANLIST; 313 } 314 if (fieldDesc.equals("Ljava/util/Set;")) { 315 return dbArray ? LINKEDHASHSET : BEANSET; 316 } 317 if (fieldDesc.equals("Ljava/util/Map;")) { 318 return dbArray ? LINKEDHASHMAP : BEANMAP; 319 } 320 return null; 321 } 322 323 /** 324 * Add a get field method with interception. 325 */ 326 private void addGet(ClassVisitor cw, ClassMeta classMeta) { 327 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), getMethodName, getMethodDesc, null, null); 328 mv.visitCode(); 329 330 if (isInitMany()) { 331 addGetForMany(mv); 332 return; 333 } 334 335 // ARETURN or IRETURN 336 int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN); 337 338 String className = classMeta.getClassName(); 339 340 Label labelEnd = new Label(); 341 Label labelStart = null; 342 343 int maxVars = 1; 344 if (isId()) { 345 labelStart = new Label(); 346 mv.visitLabel(labelStart); 347 mv.visitLineNumber(5, labelStart); 348 mv.visitVarInsn(ALOAD, 0); 349 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 350 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetId", NOARG_VOID, false); 351 352 } else if (isInterceptGet()) { 353 maxVars = 2; 354 labelStart = new Label(); 355 mv.visitLabel(labelStart); 356 mv.visitLineNumber(6, labelStart); 357 mv.visitVarInsn(ALOAD, 0); 358 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 359 VisitUtil.visitIntInsn(mv, indexPosition); 360 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false); 361 } 362 if (labelStart == null) { 363 labelStart = labelEnd; 364 } 365 mv.visitLabel(labelEnd); 366 mv.visitLineNumber(7, labelEnd); 367 mv.visitVarInsn(ALOAD, 0); 368 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 369 mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN 370 Label labelEnd1 = new Label(); 371 mv.visitLabel(labelEnd1); 372 mv.visitLocalVariable("this", "L" + className + ";", null, labelStart, labelEnd1, 0); 373 mv.visitMaxs(maxVars, 1); 374 mv.visitEnd(); 375 } 376 377 private void addGetForMany(MethodVisitor mv) { 378 String className = classMeta.getClassName(); 379 String ebCollection = getInitCollectionClass(); 380 381 Label l0 = new Label(); 382 mv.visitLabel(l0); 383 mv.visitLineNumber(1, l0); 384 mv.visitVarInsn(ALOAD, 0); 385 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 386 VisitUtil.visitIntInsn(mv, indexPosition); 387 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false); 388 389 Label l4 = new Label(); 390 if (classMeta.getEnhanceContext().isCheckNullManyFields()) { 391 if (ebCollection == null) { 392 String msg = "Unexpected collection type [" + Type.getType(fieldDesc).getClassName() + "] for [" 393 + classMeta.getClassName() + "." + fieldName + "] expected either java.util.List, java.util.Set or java.util.Map "; 394 throw new RuntimeException(msg); 395 } 396 Label l3 = new Label(); 397 mv.visitLabel(l3); 398 mv.visitLineNumber(2, l3); 399 mv.visitVarInsn(ALOAD, 0); 400 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 401 402 mv.visitJumpInsn(IFNONNULL, l4); 403 Label l5 = new Label(); 404 mv.visitLabel(l5); 405 mv.visitLineNumber(3, l5); 406 mv.visitVarInsn(ALOAD, 0); 407 mv.visitTypeInsn(NEW, ebCollection); 408 mv.visitInsn(DUP); 409 mv.visitMethodInsn(INVOKESPECIAL, ebCollection, INIT, NOARG_VOID, false); 410 mv.visitFieldInsn(PUTFIELD, className, fieldName, fieldDesc); 411 412 mv.visitVarInsn(ALOAD, 0); 413 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 414 VisitUtil.visitIntInsn(mv, indexPosition); 415 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "initialisedMany", "(I)V", false); 416 417 if (isManyToMany()) { 418 // turn on modify listening for ManyToMany 419 Label l6 = new Label(); 420 mv.visitLabel(l6); 421 mv.visitLineNumber(4, l6); 422 mv.visitVarInsn(ALOAD, 0); 423 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 424 mv.visitTypeInsn(CHECKCAST, C_BEANCOLLECTION); 425 mv.visitFieldInsn(GETSTATIC, C_BEANCOLLECTION + "$ModifyListenMode", "ALL", "L" + C_BEANCOLLECTION + "$ModifyListenMode;"); 426 mv.visitMethodInsn(INVOKEINTERFACE, C_BEANCOLLECTION, "setModifyListening", "(L" + C_BEANCOLLECTION + "$ModifyListenMode;)V", true); 427 } 428 } 429 430 mv.visitLabel(l4); 431 mv.visitLineNumber(5, l4); 432 mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); 433 mv.visitVarInsn(ALOAD, 0); 434 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 435 mv.visitInsn(ARETURN); 436 Label l7 = new Label(); 437 mv.visitLabel(l7); 438 mv.visitLocalVariable("this", "L" + className + ";", null, l0, l7, 0); 439 mv.visitMaxs(3, 1); 440 mv.visitEnd(); 441 } 442 443 /** 444 * This is a get method with no interception. 445 * <p> 446 * It exists to be able to read private fields that are on super classes. 447 * </p> 448 */ 449 private void addGetNoIntercept(ClassVisitor cw, ClassMeta classMeta) { 450 // ARETURN or IRETURN 451 int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN); 452 453 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), getNoInterceptMethodName, getMethodDesc, null, null); 454 mv.visitCode(); 455 456 Label l0 = new Label(); 457 mv.visitLabel(l0); 458 mv.visitLineNumber(1, l0); 459 mv.visitVarInsn(ALOAD, 0); 460 mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc); 461 mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN 462 Label l2 = new Label(); 463 mv.visitLabel(l2); 464 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l2, 0); 465 mv.visitMaxs(2, 1); 466 mv.visitEnd(); 467 } 468 469 /** 470 * Setter method with interception. 471 * <pre> 472 * public void _ebean_set_propname(String newValue) { 473 * ebi.preSetter(true, propertyIndex, _ebean_get_propname(), newValue); 474 * this.propname = newValue; 475 * } 476 * </pre> 477 */ 478 private void addSet(ClassVisitor cw, ClassMeta classMeta) { 479 String preSetterArgTypes = "Ljava/lang/Object;Ljava/lang/Object;"; 480 if (!objectType) { 481 // preSetter method overloaded for primitive type comparison 482 preSetterArgTypes = fieldDesc + fieldDesc; 483 } 484 485 // ALOAD or ILOAD etc 486 int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD); 487 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), setMethodName, setMethodDesc, null, null); 488 mv.visitCode(); 489 490 Label l0 = new Label(); 491 mv.visitLabel(l0); 492 mv.visitLineNumber(1, l0); 493 mv.visitVarInsn(ALOAD, 0); 494 mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT); 495 if (isInterceptSet()) { 496 mv.visitInsn(ICONST_1); 497 } else { 498 // id or OneToMany field etc 499 mv.visitInsn(ICONST_0); 500 } 501 VisitUtil.visitIntInsn(mv, indexPosition); 502 mv.visitVarInsn(ALOAD, 0); 503 if (isId() || isToManyGetField(classMeta)) { 504 // skip getter on Id as we now intercept that via preGetId() for automatic jdbc batch flushing 505 mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc); 506 } else { 507 mv.visitMethodInsn(INVOKEVIRTUAL, fieldClass, getMethodName, getMethodDesc, false); 508 } 509 mv.visitVarInsn(iLoadOpcode, 1); 510 String preSetterMethod = "preSetter"; 511 if (isToMany()) { 512 preSetterMethod = "preSetterMany"; 513 } 514 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, preSetterMethod, "(ZI" + preSetterArgTypes + ")V", false); 515 Label l1 = new Label(); 516 mv.visitLabel(l1); 517 mv.visitLineNumber(2, l1); 518 mv.visitVarInsn(ALOAD, 0); 519 mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD 520 mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc); 521 522 Label l3 = new Label(); 523 mv.visitLabel(l3); 524 mv.visitLineNumber(4, l3); 525 mv.visitInsn(RETURN); 526 Label l4 = new Label(); 527 mv.visitLabel(l4); 528 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l4, 0); 529 mv.visitLocalVariable("newValue", fieldDesc, null, l0, l4, 1); 530 mv.visitMaxs(5, 2); 531 mv.visitEnd(); 532 } 533 534 private boolean isToManyGetField(ClassMeta meta) { 535 return isToMany() && meta.isToManyGetField(); 536 } 537 538 /** 539 * Add a non-intercepting field set method. 540 * <p> 541 * So we can set private fields on super classes. 542 * </p> 543 */ 544 private void addSetNoIntercept(ClassVisitor cw, ClassMeta classMeta) { 545 // ALOAD or ILOAD etc 546 int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD); 547 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), setNoInterceptMethodName, setMethodDesc, null, null); 548 mv.visitCode(); 549 Label l0 = new Label(); 550 551 mv.visitLabel(l0); 552 mv.visitLineNumber(1, l0); 553 mv.visitVarInsn(ALOAD, 0); 554 mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD 555 mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc); 556 557 Label l1 = new Label(); 558 mv.visitLabel(l1); 559 mv.visitLineNumber(2, l1); 560 mv.visitVarInsn(ALOAD, 0); 561 mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT); 562 VisitUtil.visitIntInsn(mv, indexPosition); 563 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "setLoadedProperty", "(I)V", false); 564 565 Label l2 = new Label(); 566 mv.visitLabel(l2); 567 mv.visitLineNumber(1, l2); 568 mv.visitInsn(RETURN); 569 Label l3 = new Label(); 570 mv.visitLabel(l3); 571 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l3, 0); 572 mv.visitLocalVariable("_newValue", fieldDesc, null, l0, l3, 1); 573 mv.visitMaxs(4, 2); 574 mv.visitEnd(); 575 } 576 577}