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