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 } 144 145 /** 146 * Return true if this is an ID field. 147 * <p> 148 * ID fields are used in generating equals() logic based on identity. 149 * </p> 150 */ 151 public boolean isId() { 152 return (annotations.contains("Ljavax/persistence/Id;") 153 || annotations.contains("Ljavax/persistence/EmbeddedId;")); 154 } 155 156 /** 157 * Return true if this is a OneToMany or ManyToMany field. 158 */ 159 public boolean isToMany() { 160 return annotations.contains("Ljavax/persistence/OneToMany;") 161 || annotations.contains("Ljavax/persistence/ManyToMany;"); 162 } 163 164 private boolean isManyToMany() { 165 return annotations.contains("Ljavax/persistence/ManyToMany;"); 166 } 167 168 /** 169 * Control initialisation of ToMany and DbArray collection properties. 170 * This means these properties are lazy initialised on demand. 171 */ 172 public boolean isInitMany() { 173 return isToMany() || isDbArray(); 174 } 175 176 private boolean isDbArray() { 177 return annotations.contains("Lio/ebean/annotation/DbArray;"); 178 } 179 180 /** 181 * Return true if this is an Embedded field. 182 */ 183 boolean isEmbedded() { 184 return annotations.contains("Ljavax/persistence/Embedded;"); 185 } 186 187 boolean hasOrderColumn() { 188 return annotations.contains("Ljavax/persistence/OrderColumn;"); 189 } 190 191 /** 192 * Return true if the field is local to this class. Returns false if the field 193 * is actually on a super class. 194 */ 195 boolean isLocalField(ClassMeta classMeta) { 196 return fieldClass.equals(classMeta.getClassName()); 197 } 198 199 /** 200 * Append byte code to return the Id value (for primitives). 201 */ 202 void appendGetPrimitiveIdValue(MethodVisitor mv, ClassMeta classMeta) { 203 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false); 204 } 205 206 /** 207 * Append compare instructions if its a long, float or double. 208 */ 209 void appendCompare(MethodVisitor mv, ClassMeta classMeta) { 210 if (primitiveType) { 211 if (classMeta.isLog(4)) { 212 classMeta.log(" ... getIdentity compare primitive field[" + fieldName + "] type[" + fieldDesc + "]"); 213 } 214 if (fieldDesc.equals("J")) { 215 // long compare to 0 216 mv.visitInsn(LCONST_0); 217 mv.visitInsn(LCMP); 218 219 } else if (fieldDesc.equals("D")) { 220 // double compare to 0 221 mv.visitInsn(DCONST_0); 222 mv.visitInsn(DCMPL); 223 224 } else if (fieldDesc.equals("F")) { 225 // float compare to 0 226 mv.visitInsn(FCONST_0); 227 mv.visitInsn(FCMPL); 228 } 229 // no extra instructions required for 230 // int, short, byte, char 231 } 232 } 233 234 /** 235 * Append code to get the Object value of a primitive. 236 * <p> 237 * This becomes a Integer.valueOf(someInt); or similar. 238 * </p> 239 */ 240 void appendValueOf(MethodVisitor mv) { 241 if (primitiveType) { 242 // use valueOf methods to return primitives as objects 243 Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType); 244 String objDesc = objectWrapperType.getInternalName(); 245 String primDesc = asmType.getDescriptor(); 246 mv.visitMethodInsn(Opcodes.INVOKESTATIC, objDesc, "valueOf", "(" + primDesc + ")L" + objDesc + ";", false); 247 } 248 } 249 250 /** 251 * As part of the switch statement to read the fields generate the get code. 252 */ 253 void appendSwitchGet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) { 254 if (intercept) { 255 // use the special get method with interception... 256 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getMethodName, getMethodDesc, false); 257 } else { 258 if (isLocalField(classMeta)) { 259 mv.visitFieldInsn(GETFIELD, classMeta.getClassName(), fieldName, fieldDesc); 260 } else { 261 // field is on a superclass... so use virtual getNoInterceptMethodName 262 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), getNoInterceptMethodName, getMethodDesc, false); 263 } 264 } 265 if (primitiveType) { 266 appendValueOf(mv); 267 } 268 } 269 270 void appendSwitchSet(MethodVisitor mv, ClassMeta classMeta, boolean intercept) { 271 if (primitiveType) { 272 // convert Object to primitive first... 273 Type objectWrapperType = PrimitiveHelper.getObjectWrapper(asmType); 274 String primDesc = asmType.getDescriptor(); 275 String primType = asmType.getClassName(); 276 String objInt = objectWrapperType.getInternalName(); 277 mv.visitTypeInsn(CHECKCAST, objInt); 278 mv.visitMethodInsn(INVOKEVIRTUAL, objInt, primType + "Value", "()" + primDesc, false); 279 } else { 280 // check correct object type 281 mv.visitTypeInsn(CHECKCAST, asmType.getInternalName()); 282 } 283 284 if (intercept) { 285 // go through the set method to check for interception... 286 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setMethodName, setMethodDesc, false); 287 } else { 288 mv.visitMethodInsn(INVOKEVIRTUAL, classMeta.getClassName(), setNoInterceptMethodName, setMethodDesc, false); 289 } 290 } 291 292 /** 293 * Add get and set methods for field access/interception. 294 */ 295 public void addGetSetMethods(ClassVisitor cv, ClassMeta classMeta) { 296 if (!isLocalField(classMeta)) { 297 String msg = "ERROR: " + fieldClass + " != " + classMeta.getClassName() + " for field " 298 + fieldName + " " + fieldDesc; 299 throw new RuntimeException(msg); 300 } 301 // add intercepting methods that are used to replace the 302 // standard GETFIELD PUTFIELD byte codes for field access 303 addGet(cv, classMeta); 304 addSet(cv, classMeta); 305 306 // add non-interception methods... so that we can get access 307 // to private fields on super classes 308 addGetNoIntercept(cv, classMeta); 309 addSetNoIntercept(cv, classMeta); 310 } 311 312 private String getInitCollectionClass() { 313 final boolean dbArray = isDbArray(); 314 if (fieldDesc.equals("Ljava/util/List;")) { 315 return dbArray ? ARRAYLIST : BEANLIST; 316 } 317 if (fieldDesc.equals("Ljava/util/Set;")) { 318 return dbArray ? LINKEDHASHSET : BEANSET; 319 } 320 if (fieldDesc.equals("Ljava/util/Map;")) { 321 return dbArray ? LINKEDHASHMAP : BEANMAP; 322 } 323 return null; 324 } 325 326 /** 327 * Add a get field method with interception. 328 */ 329 private void addGet(ClassVisitor cw, ClassMeta classMeta) { 330 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), getMethodName, getMethodDesc, null, null); 331 mv.visitCode(); 332 333 if (isInitMany()) { 334 addGetForMany(mv); 335 return; 336 } 337 338 // ARETURN or IRETURN 339 int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN); 340 341 String className = classMeta.getClassName(); 342 343 Label labelEnd = new Label(); 344 Label labelStart = null; 345 346 int maxVars = 1; 347 if (isId()) { 348 labelStart = new Label(); 349 mv.visitLabel(labelStart); 350 mv.visitLineNumber(5, labelStart); 351 mv.visitVarInsn(ALOAD, 0); 352 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 353 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetId", NOARG_VOID, false); 354 355 } else if (isInterceptGet()) { 356 maxVars = 2; 357 labelStart = new Label(); 358 mv.visitLabel(labelStart); 359 mv.visitLineNumber(6, labelStart); 360 mv.visitVarInsn(ALOAD, 0); 361 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 362 VisitUtil.visitIntInsn(mv, indexPosition); 363 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false); 364 } 365 if (labelStart == null) { 366 labelStart = labelEnd; 367 } 368 mv.visitLabel(labelEnd); 369 mv.visitLineNumber(7, labelEnd); 370 mv.visitVarInsn(ALOAD, 0); 371 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 372 mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN 373 Label labelEnd1 = new Label(); 374 mv.visitLabel(labelEnd1); 375 mv.visitLocalVariable("this", "L" + className + ";", null, labelStart, labelEnd1, 0); 376 mv.visitMaxs(maxVars, 1); 377 mv.visitEnd(); 378 } 379 380 private void addGetForMany(MethodVisitor mv) { 381 String className = classMeta.getClassName(); 382 String ebCollection = getInitCollectionClass(); 383 384 Label l0 = new Label(); 385 mv.visitLabel(l0); 386 mv.visitLineNumber(1, l0); 387 mv.visitVarInsn(ALOAD, 0); 388 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 389 VisitUtil.visitIntInsn(mv, indexPosition); 390 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "preGetter", "(I)V", false); 391 392 Label l4 = new Label(); 393 if (classMeta.getEnhanceContext().isCheckNullManyFields()) { 394 if (ebCollection == null) { 395 String msg = "Unexpected collection type [" + Type.getType(fieldDesc).getClassName() + "] for [" 396 + classMeta.getClassName() + "." + fieldName + "] expected either java.util.List, java.util.Set or java.util.Map "; 397 throw new RuntimeException(msg); 398 } 399 Label l3 = new Label(); 400 mv.visitLabel(l3); 401 mv.visitLineNumber(2, l3); 402 mv.visitVarInsn(ALOAD, 0); 403 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 404 405 mv.visitJumpInsn(IFNONNULL, l4); 406 Label l5 = new Label(); 407 mv.visitLabel(l5); 408 mv.visitLineNumber(3, l5); 409 mv.visitVarInsn(ALOAD, 0); 410 mv.visitTypeInsn(NEW, ebCollection); 411 mv.visitInsn(DUP); 412 mv.visitMethodInsn(INVOKESPECIAL, ebCollection, INIT, NOARG_VOID, false); 413 mv.visitFieldInsn(PUTFIELD, className, fieldName, fieldDesc); 414 415 mv.visitVarInsn(ALOAD, 0); 416 mv.visitFieldInsn(GETFIELD, className, INTERCEPT_FIELD, L_INTERCEPT); 417 VisitUtil.visitIntInsn(mv, indexPosition); 418 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "initialisedMany", "(I)V", false); 419 420 if (isManyToMany() || hasOrderColumn()) { 421 // turn on modify listening for ManyToMany 422 Label l6 = new Label(); 423 mv.visitLabel(l6); 424 mv.visitLineNumber(4, l6); 425 mv.visitVarInsn(ALOAD, 0); 426 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 427 mv.visitTypeInsn(CHECKCAST, C_BEANCOLLECTION); 428 mv.visitFieldInsn(GETSTATIC, C_BEANCOLLECTION + "$ModifyListenMode", "ALL", "L" + C_BEANCOLLECTION + "$ModifyListenMode;"); 429 mv.visitMethodInsn(INVOKEINTERFACE, C_BEANCOLLECTION, "setModifyListening", "(L" + C_BEANCOLLECTION + "$ModifyListenMode;)V", true); 430 } 431 } 432 433 mv.visitLabel(l4); 434 mv.visitLineNumber(5, l4); 435 mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); 436 mv.visitVarInsn(ALOAD, 0); 437 mv.visitFieldInsn(GETFIELD, className, fieldName, fieldDesc); 438 mv.visitInsn(ARETURN); 439 Label l7 = new Label(); 440 mv.visitLabel(l7); 441 mv.visitLocalVariable("this", "L" + className + ";", null, l0, l7, 0); 442 mv.visitMaxs(3, 1); 443 mv.visitEnd(); 444 } 445 446 /** 447 * This is a get method with no interception. 448 * <p> 449 * It exists to be able to read private fields that are on super classes. 450 * </p> 451 */ 452 private void addGetNoIntercept(ClassVisitor cw, ClassMeta classMeta) { 453 // ARETURN or IRETURN 454 int iReturnOpcode = asmType.getOpcode(Opcodes.IRETURN); 455 456 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), getNoInterceptMethodName, getMethodDesc, null, null); 457 mv.visitCode(); 458 459 Label l0 = new Label(); 460 mv.visitLabel(l0); 461 mv.visitLineNumber(1, l0); 462 mv.visitVarInsn(ALOAD, 0); 463 mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc); 464 mv.visitInsn(iReturnOpcode);// ARETURN or IRETURN 465 Label l2 = new Label(); 466 mv.visitLabel(l2); 467 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l2, 0); 468 mv.visitMaxs(2, 1); 469 mv.visitEnd(); 470 } 471 472 /** 473 * Setter method with interception. 474 * <pre> 475 * public void _ebean_set_propname(String newValue) { 476 * ebi.preSetter(true, propertyIndex, _ebean_get_propname(), newValue); 477 * this.propname = newValue; 478 * } 479 * </pre> 480 */ 481 private void addSet(ClassVisitor cw, ClassMeta classMeta) { 482 String preSetterArgTypes = "Ljava/lang/Object;Ljava/lang/Object;"; 483 if (!objectType) { 484 // preSetter method overloaded for primitive type comparison 485 preSetterArgTypes = fieldDesc + fieldDesc; 486 } 487 488 // ALOAD or ILOAD etc 489 int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD); 490 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), setMethodName, setMethodDesc, null, null); 491 mv.visitCode(); 492 493 Label l0 = new Label(); 494 mv.visitLabel(l0); 495 mv.visitLineNumber(1, l0); 496 mv.visitVarInsn(ALOAD, 0); 497 mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT); 498 if (isInterceptSet()) { 499 mv.visitInsn(ICONST_1); 500 } else { 501 // id or OneToMany field etc 502 mv.visitInsn(ICONST_0); 503 } 504 VisitUtil.visitIntInsn(mv, indexPosition); 505 mv.visitVarInsn(ALOAD, 0); 506 if (isId() || isToManyGetField(classMeta)) { 507 // skip getter on Id as we now intercept that via preGetId() for automatic jdbc batch flushing 508 mv.visitFieldInsn(GETFIELD, fieldClass, fieldName, fieldDesc); 509 } else { 510 mv.visitMethodInsn(INVOKEVIRTUAL, fieldClass, getMethodName, getMethodDesc, false); 511 } 512 mv.visitVarInsn(iLoadOpcode, 1); 513 String preSetterMethod = "preSetter"; 514 if (isToMany()) { 515 preSetterMethod = "preSetterMany"; 516 } 517 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, preSetterMethod, "(ZI" + preSetterArgTypes + ")V", false); 518 Label l1 = new Label(); 519 mv.visitLabel(l1); 520 mv.visitLineNumber(2, l1); 521 mv.visitVarInsn(ALOAD, 0); 522 mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD 523 mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc); 524 525 Label l3 = new Label(); 526 mv.visitLabel(l3); 527 mv.visitLineNumber(4, l3); 528 mv.visitInsn(RETURN); 529 Label l4 = new Label(); 530 mv.visitLabel(l4); 531 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l4, 0); 532 mv.visitLocalVariable("newValue", fieldDesc, null, l0, l4, 1); 533 mv.visitMaxs(5, 2); 534 mv.visitEnd(); 535 } 536 537 private boolean isToManyGetField(ClassMeta meta) { 538 return isToMany() && meta.isToManyGetField(); 539 } 540 541 /** 542 * Add a non-intercepting field set method. 543 * <p> 544 * So we can set private fields on super classes. 545 * </p> 546 */ 547 private void addSetNoIntercept(ClassVisitor cw, ClassMeta classMeta) { 548 // ALOAD or ILOAD etc 549 int iLoadOpcode = asmType.getOpcode(Opcodes.ILOAD); 550 MethodVisitor mv = cw.visitMethod(classMeta.accProtected(), setNoInterceptMethodName, setMethodDesc, null, null); 551 mv.visitCode(); 552 Label l0 = new Label(); 553 554 mv.visitLabel(l0); 555 mv.visitLineNumber(1, l0); 556 mv.visitVarInsn(ALOAD, 0); 557 mv.visitVarInsn(iLoadOpcode, 1);// ALOAD or ILOAD 558 mv.visitFieldInsn(PUTFIELD, fieldClass, fieldName, fieldDesc); 559 560 Label l1 = new Label(); 561 mv.visitLabel(l1); 562 mv.visitLineNumber(2, l1); 563 mv.visitVarInsn(ALOAD, 0); 564 mv.visitFieldInsn(GETFIELD, fieldClass, INTERCEPT_FIELD, L_INTERCEPT); 565 VisitUtil.visitIntInsn(mv, indexPosition); 566 mv.visitMethodInsn(INVOKEVIRTUAL, C_INTERCEPT, "setLoadedProperty", "(I)V", false); 567 568 Label l2 = new Label(); 569 mv.visitLabel(l2); 570 mv.visitLineNumber(1, l2); 571 mv.visitInsn(RETURN); 572 Label l3 = new Label(); 573 mv.visitLabel(l3); 574 mv.visitLocalVariable("this", "L" + fieldClass + ";", null, l0, l3, 0); 575 mv.visitLocalVariable("_newValue", fieldDesc, null, l0, l3, 1); 576 mv.visitMaxs(4, 2); 577 mv.visitEnd(); 578 } 579 580}