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