View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.bcel.util;
20  
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.nio.charset.Charset;
24  import java.util.BitSet;
25  
26  import org.apache.bcel.Const;
27  import org.apache.bcel.classfile.Attribute;
28  import org.apache.bcel.classfile.ClassFormatException;
29  import org.apache.bcel.classfile.Code;
30  import org.apache.bcel.classfile.CodeException;
31  import org.apache.bcel.classfile.ConstantFieldref;
32  import org.apache.bcel.classfile.ConstantInterfaceMethodref;
33  import org.apache.bcel.classfile.ConstantInvokeDynamic;
34  import org.apache.bcel.classfile.ConstantMethodref;
35  import org.apache.bcel.classfile.ConstantNameAndType;
36  import org.apache.bcel.classfile.ConstantPool;
37  import org.apache.bcel.classfile.LocalVariableTable;
38  import org.apache.bcel.classfile.Method;
39  import org.apache.bcel.classfile.Utility;
40  
41  /**
42   * Convert code into HTML file.
43   */
44  final class CodeHTML {
45  
46      private static boolean wide;
47      private final String className; // name of current class
48      // private Method[] methods; // Methods to print
49      private final PrintWriter printWriter; // file to write to
50      private BitSet gotoSet;
51      private final ConstantPool constantPool;
52      private final ConstantHTML constantHtml;
53  
54      CodeHTML(final String dir, final String className, final Method[] methods, final ConstantPool constantPool, final ConstantHTML constantHtml,
55          final Charset charset) throws IOException {
56          this.className = className;
57  //        this.methods = methods;
58          this.constantPool = constantPool;
59          this.constantHtml = constantHtml;
60          try (PrintWriter newPrintWriter = new PrintWriter(dir + className + "_code.html", charset.name())) {
61              printWriter = newPrintWriter;
62              printWriter.print("<HTML><head><meta charset=\"");
63              printWriter.print(charset.name());
64              printWriter.println("\"></head>");
65              printWriter.println("<BODY BGCOLOR=\"#C0C0C0\">");
66              for (int i = 0; i < methods.length; i++) {
67                  writeMethod(methods[i], i);
68              }
69              printWriter.println("</BODY></HTML>");
70          }
71      }
72  
73      /**
74       * Disassemble a stream of byte codes and return the string representation.
75       *
76       * @param stream data input stream.
77       * @return String representation of byte code.
78       */
79      private String codeToHTML(final ByteSequence bytes, final int methodNumber) throws IOException {
80          final short opcode = (short) bytes.readUnsignedByte();
81          String name;
82          final String signature;
83          int defaultOffset = 0;
84          final int low;
85          final int high;
86          int index;
87          final int classIndex;
88          final int vindex;
89          final int constant;
90          final int[] jumpTable;
91          int noPadBytes = 0;
92          final int offset;
93          final StringBuilder buf = new StringBuilder(256); // CHECKSTYLE IGNORE MagicNumber
94          buf.append("<TT>").append(Const.getOpcodeName(opcode)).append("</TT></TD><TD>");
95          /*
96           * Special case: Skip (0-3) padding bytes, that is, the following bytes are 4-byte-aligned
97           */
98          if (opcode == Const.TABLESWITCH || opcode == Const.LOOKUPSWITCH) {
99              final int remainder = bytes.getIndex() % 4;
100             noPadBytes = remainder == 0 ? 0 : 4 - remainder;
101             for (int i = 0; i < noPadBytes; i++) {
102                 bytes.readByte();
103             }
104             // Both cases have a field default_offset in common
105             defaultOffset = bytes.readInt();
106         }
107         switch (opcode) {
108         case Const.TABLESWITCH:
109             low = bytes.readInt();
110             high = bytes.readInt();
111             offset = bytes.getIndex() - 12 - noPadBytes - 1;
112             defaultOffset += offset;
113             // Each jump table entry is a 4 byte offset, so a well-formed table cannot declare more entries than fit
114             // into the remaining byte code; checking before allocating keeps a crafted low/high pair from forcing a
115             // huge allocation.
116             final long jumpTableLength = (long) high - low + 1;
117             if (jumpTableLength < 0 || jumpTableLength * 4 > bytes.available()) {
118                 throw new ClassFormatException("Invalid TABLESWITCH: low = " + low + ", high = " + high + " but only " + bytes.available() + " bytes remain");
119             }
120             buf.append("<TABLE BORDER=1><TR>");
121             // Print switch indices in first row (and default)
122             jumpTable = new int[(int) jumpTableLength];
123             for (int i = 0; i < jumpTable.length; i++) {
124                 jumpTable[i] = offset + bytes.readInt();
125                 buf.append("<TH>").append(low + i).append("</TH>");
126             }
127             buf.append("<TH>default</TH></TR>\n<TR>");
128             // Print target and default indices in second row
129             for (final int element : jumpTable) {
130                 buf.append("<TD><A HREF=\"#code").append(methodNumber).append("@").append(element).append("\">").append(element).append("</A></TD>");
131             }
132             buf.append("<TD><A HREF=\"#code").append(methodNumber).append("@").append(defaultOffset).append("\">").append(defaultOffset)
133                 .append("</A></TD></TR>\n</TABLE>\n");
134             break;
135         /*
136          * Lookup switch has variable length arguments.
137          */
138         case Const.LOOKUPSWITCH:
139             final int npairs = bytes.readInt();
140             offset = bytes.getIndex() - 8 - noPadBytes - 1;
141             // Each match-offset pair is 8 bytes, see the TABLESWITCH check above.
142             if (npairs < 0 || (long) npairs * 8 > bytes.available()) {
143                 throw new ClassFormatException("Invalid LOOKUPSWITCH: npairs = " + npairs + " but only " + bytes.available() + " bytes remain");
144             }
145             jumpTable = new int[npairs];
146             defaultOffset += offset;
147             buf.append("<TABLE BORDER=1><TR>");
148             // Print switch indices in first row (and default)
149             for (int i = 0; i < npairs; i++) {
150                 final int match = bytes.readInt();
151                 jumpTable[i] = offset + bytes.readInt();
152                 buf.append("<TH>").append(match).append("</TH>");
153             }
154             buf.append("<TH>default</TH></TR>\n<TR>");
155             // Print target and default indices in second row
156             for (int i = 0; i < npairs; i++) {
157                 buf.append("<TD><A HREF=\"#code").append(methodNumber).append("@").append(jumpTable[i]).append("\">").append(jumpTable[i])
158                     .append("</A></TD>");
159             }
160             buf.append("<TD><A HREF=\"#code").append(methodNumber).append("@").append(defaultOffset).append("\">").append(defaultOffset)
161                 .append("</A></TD></TR>\n</TABLE>\n");
162             break;
163         /*
164          * Two address bytes + offset from start of byte stream form the jump target.
165          */
166         case Const.GOTO:
167         case Const.IFEQ:
168         case Const.IFGE:
169         case Const.IFGT:
170         case Const.IFLE:
171         case Const.IFLT:
172         case Const.IFNE:
173         case Const.IFNONNULL:
174         case Const.IFNULL:
175         case Const.IF_ACMPEQ:
176         case Const.IF_ACMPNE:
177         case Const.IF_ICMPEQ:
178         case Const.IF_ICMPGE:
179         case Const.IF_ICMPGT:
180         case Const.IF_ICMPLE:
181         case Const.IF_ICMPLT:
182         case Const.IF_ICMPNE:
183         case Const.JSR:
184             index = bytes.getIndex() + bytes.readShort() - 1;
185             buf.append("<A HREF=\"#code").append(methodNumber).append("@").append(index).append("\">").append(index).append("</A>");
186             break;
187         /*
188          * Same for 32-bit wide jumps
189          */
190         case Const.GOTO_W:
191         case Const.JSR_W:
192             final int windex = bytes.getIndex() + bytes.readInt() - 1;
193             buf.append("<A HREF=\"#code").append(methodNumber).append("@").append(windex).append("\">").append(windex).append("</A>");
194             break;
195         /*
196          * Index byte references local variable (register)
197          */
198         case Const.ALOAD:
199         case Const.ASTORE:
200         case Const.DLOAD:
201         case Const.DSTORE:
202         case Const.FLOAD:
203         case Const.FSTORE:
204         case Const.ILOAD:
205         case Const.ISTORE:
206         case Const.LLOAD:
207         case Const.LSTORE:
208         case Const.RET:
209             if (wide) {
210                 vindex = bytes.readUnsignedShort();
211                 wide = false; // Clear flag
212             } else {
213                 vindex = bytes.readUnsignedByte();
214             }
215             buf.append("%").append(vindex);
216             break;
217         /*
218          * Remember wide byte which is used to form a 16-bit address in the following instruction. Relies on that the method is
219          * called again with the following opcode.
220          */
221         case Const.WIDE:
222             wide = true;
223             buf.append("(wide)");
224             break;
225         /*
226          * Array of basic type.
227          */
228         case Const.NEWARRAY:
229             buf.append("<FONT COLOR=\"#00FF00\">").append(Const.getTypeName(bytes.readByte())).append("</FONT>");
230             break;
231         /*
232          * Access object/class fields.
233          */
234         case Const.GETFIELD:
235         case Const.GETSTATIC:
236         case Const.PUTFIELD:
237         case Const.PUTSTATIC:
238             index = bytes.readUnsignedShort();
239             final ConstantFieldref c1 = constantPool.getConstant(index, Const.CONSTANT_Fieldref, ConstantFieldref.class);
240             classIndex = c1.getClassIndex();
241             name = constantPool.getConstantString(classIndex, Const.CONSTANT_Class);
242             name = Utility.compactClassName(name, false);
243             index = c1.getNameAndTypeIndex();
244             final String fieldName = constantPool.constantToString(index, Const.CONSTANT_NameAndType);
245             if (name.equals(className)) { // Local field
246                 buf.append("<A HREF=\"").append(className).append("_methods.html#field").append(Class2HTML.toHTML(fieldName)).append("\" TARGET=Methods>")
247                     .append(Class2HTML.toHTML(fieldName)).append("</A>\n");
248             } else {
249                 buf.append(constantHtml.referenceConstant(classIndex)).append(".").append(Class2HTML.toHTML(fieldName));
250             }
251             break;
252         /*
253          * Operands are references to classes in constant pool
254          */
255         case Const.CHECKCAST:
256         case Const.INSTANCEOF:
257         case Const.NEW:
258             index = bytes.readUnsignedShort();
259             buf.append(constantHtml.referenceConstant(index));
260             break;
261         /*
262          * Operands are references to methods in constant pool
263          */
264         case Const.INVOKESPECIAL:
265         case Const.INVOKESTATIC:
266         case Const.INVOKEVIRTUAL:
267         case Const.INVOKEINTERFACE:
268         case Const.INVOKEDYNAMIC:
269             final int mIndex = bytes.readUnsignedShort();
270             final String str;
271             if (opcode == Const.INVOKEINTERFACE) { // Special treatment needed
272                 bytes.readUnsignedByte(); // Redundant
273                 bytes.readUnsignedByte(); // Reserved
274 //                    int nargs = bytes.readUnsignedByte(); // Redundant
275 //                    int reserved = bytes.readUnsignedByte(); // Reserved
276                 final ConstantInterfaceMethodref c = constantPool.getConstant(mIndex, Const.CONSTANT_InterfaceMethodref, ConstantInterfaceMethodref.class);
277                 classIndex = c.getClassIndex();
278                 index = c.getNameAndTypeIndex();
279                 name = Class2HTML.referenceClass(classIndex);
280             } else if (opcode == Const.INVOKEDYNAMIC) { // Special treatment needed
281                 bytes.readUnsignedByte(); // Reserved
282                 bytes.readUnsignedByte(); // Reserved
283                 final ConstantInvokeDynamic c = constantPool.getConstant(mIndex, Const.CONSTANT_InvokeDynamic, ConstantInvokeDynamic.class);
284                 index = c.getNameAndTypeIndex();
285                 name = "#" + c.getBootstrapMethodAttrIndex();
286             } else {
287                 // UNDONE: Java8 now allows INVOKESPECIAL and INVOKESTATIC to
288                 // reference EITHER a Methodref OR an InterfaceMethodref.
289                 // Not sure if that affects this code or not. (markro)
290                 final ConstantMethodref c = constantPool.getConstant(mIndex, Const.CONSTANT_Methodref, ConstantMethodref.class);
291                 classIndex = c.getClassIndex();
292                 index = c.getNameAndTypeIndex();
293                 name = Class2HTML.referenceClass(classIndex);
294             }
295             str = Class2HTML.toHTML(constantPool.constantToString(constantPool.getConstant(index, Const.CONSTANT_NameAndType)));
296             // Get signature, that is, types
297             final ConstantNameAndType c2 = constantPool.getConstant(index, Const.CONSTANT_NameAndType, ConstantNameAndType.class);
298             signature = constantPool.constantToString(c2.getSignatureIndex(), Const.CONSTANT_Utf8);
299             final String[] args = Utility.methodSignatureArgumentTypes(signature, false);
300             final String type = Utility.methodSignatureReturnType(signature, false);
301             buf.append(name).append(".<A HREF=\"").append(className).append("_cp.html#cp").append(mIndex).append("\" TARGET=ConstantPool>").append(str)
302                 .append("</A>").append("(");
303             // List arguments
304             for (int i = 0; i < args.length; i++) {
305                 buf.append(Class2HTML.referenceType(args[i]));
306                 if (i < args.length - 1) {
307                     buf.append(", ");
308                 }
309             }
310             // Attach return type
311             buf.append("):").append(Class2HTML.referenceType(type));
312             break;
313         /*
314          * Operands are references to items in constant pool
315          */
316         case Const.LDC_W:
317         case Const.LDC2_W:
318             index = bytes.readUnsignedShort();
319             buf.append("<A HREF=\"").append(className).append("_cp.html#cp").append(index).append("\" TARGET=\"ConstantPool\">")
320                 .append(Class2HTML.toHTML(constantPool.constantToString(index, constantPool.getConstant(index).getTag()))).append("</a>");
321             break;
322         case Const.LDC:
323             index = bytes.readUnsignedByte();
324             buf.append("<A HREF=\"").append(className).append("_cp.html#cp").append(index).append("\" TARGET=\"ConstantPool\">")
325                 .append(Class2HTML.toHTML(constantPool.constantToString(index, constantPool.getConstant(index).getTag()))).append("</a>");
326             break;
327         /*
328          * Array of references.
329          */
330         case Const.ANEWARRAY:
331             index = bytes.readUnsignedShort();
332             buf.append(constantHtml.referenceConstant(index));
333             break;
334         /*
335          * Multidimensional array of references.
336          */
337         case Const.MULTIANEWARRAY:
338             index = bytes.readUnsignedShort();
339             final int dimensions = bytes.readUnsignedByte();
340             buf.append(constantHtml.referenceConstant(index)).append(":").append(dimensions).append("-dimensional");
341             break;
342         /*
343          * Increment local variable.
344          */
345         case Const.IINC:
346             if (wide) {
347                 vindex = bytes.readUnsignedShort();
348                 constant = bytes.readShort();
349                 wide = false;
350             } else {
351                 vindex = bytes.readUnsignedByte();
352                 constant = bytes.readByte();
353             }
354             buf.append("%").append(vindex).append(" ").append(constant);
355             break;
356         default:
357             if (Const.getNoOfOperands(opcode) > 0) {
358                 for (int i = 0; i < Const.getOperandTypeCount(opcode); i++) {
359                     switch (Const.getOperandType(opcode, i)) {
360                     case Const.T_BYTE:
361                         buf.append(bytes.readUnsignedByte());
362                         break;
363                     case Const.T_SHORT: // Either branch or index
364                         buf.append(bytes.readShort());
365                         break;
366                     case Const.T_INT:
367                         buf.append(bytes.readInt());
368                         break;
369                     default: // Never reached
370                         throw new IllegalStateException("Unreachable default case reached! " + Const.getOperandType(opcode, i));
371                     }
372                     buf.append("&nbsp;");
373                 }
374             }
375         }
376         buf.append("</TD>");
377         return buf.toString();
378     }
379 
380     /**
381      * Find all target addresses in code, so that they can be marked with &lt;A NAME = ...&gt;. Target addresses are kept in
382      * an BitSet object.
383      */
384     private void findGotos(final ByteSequence bytes, final Code code) throws IOException {
385         int index;
386         gotoSet = new BitSet(bytes.available());
387         int opcode;
388         /*
389          * First get Code attribute from method and the exceptions handled (try-catch) in this method. We only need the line
390          * number here.
391          */
392         if (code != null) {
393             final CodeException[] ce = code.getExceptionTable();
394             for (final CodeException cex : ce) {
395                 gotoSet.set(cex.getStartPC());
396                 gotoSet.set(cex.getEndPC());
397                 gotoSet.set(cex.getHandlerPC());
398             }
399             // Look for local variables and their range
400             final Attribute[] attributes = code.getAttributes();
401             for (final Attribute attribute : attributes) {
402                 if (attribute.getTag() == Const.ATTR_LOCAL_VARIABLE_TABLE) {
403                     ((LocalVariableTable) attribute).forEach(var -> {
404                         final int start = var.getStartPC();
405                         gotoSet.set(start);
406                         gotoSet.set(start + var.getLength());
407                     });
408                     break;
409                 }
410             }
411         }
412         // Get target addresses from GOTO, JSR, TABLESWITCH, etc.
413         while (bytes.available() > 0) {
414             opcode = bytes.readUnsignedByte();
415             // System.out.println(getOpcodeName(opcode));
416             switch (opcode) {
417             case Const.TABLESWITCH:
418             case Const.LOOKUPSWITCH:
419                 // bytes.readByte(); // Skip already read byte
420                 final int remainder = bytes.getIndex() % 4;
421                 final int noPadBytes = remainder == 0 ? 0 : 4 - remainder;
422                 int defaultOffset;
423                 final int offset;
424                 for (int j = 0; j < noPadBytes; j++) {
425                     bytes.readByte();
426                 }
427                 // Both cases have a field default_offset in common
428                 defaultOffset = bytes.readInt();
429                 if (opcode == Const.TABLESWITCH) {
430                     final int low = bytes.readInt();
431                     final int high = bytes.readInt();
432                     offset = bytes.getIndex() - 12 - noPadBytes - 1;
433                     defaultOffset += offset;
434                     gotoSet.set(defaultOffset);
435                     for (int j = 0; j < high - low + 1; j++) {
436                         index = offset + bytes.readInt();
437                         gotoSet.set(index);
438                     }
439                 } else { // LOOKUPSWITCH
440                     final int npairs = bytes.readInt();
441                     offset = bytes.getIndex() - 8 - noPadBytes - 1;
442                     defaultOffset += offset;
443                     gotoSet.set(defaultOffset);
444                     for (int j = 0; j < npairs; j++) {
445 //                            int match = bytes.readInt();
446                         bytes.readInt();
447                         index = offset + bytes.readInt();
448                         gotoSet.set(index);
449                     }
450                 }
451                 break;
452             case Const.GOTO:
453             case Const.IFEQ:
454             case Const.IFGE:
455             case Const.IFGT:
456             case Const.IFLE:
457             case Const.IFLT:
458             case Const.IFNE:
459             case Const.IFNONNULL:
460             case Const.IFNULL:
461             case Const.IF_ACMPEQ:
462             case Const.IF_ACMPNE:
463             case Const.IF_ICMPEQ:
464             case Const.IF_ICMPGE:
465             case Const.IF_ICMPGT:
466             case Const.IF_ICMPLE:
467             case Const.IF_ICMPLT:
468             case Const.IF_ICMPNE:
469             case Const.JSR:
470                 // bytes.readByte(); // Skip already read byte
471                 index = bytes.getIndex() + bytes.readShort() - 1;
472                 gotoSet.set(index);
473                 break;
474             case Const.GOTO_W:
475             case Const.JSR_W:
476                 // bytes.readByte(); // Skip already read byte
477                 index = bytes.getIndex() + bytes.readInt() - 1;
478                 gotoSet.set(index);
479                 break;
480             default:
481                 bytes.unreadByte();
482                 codeToHTML(bytes, 0); // Ignore output
483             }
484         }
485     }
486 
487     /**
488      * Writes a single method with the byte code associated with it.
489      */
490     private void writeMethod(final Method method, final int methodNumber) throws IOException {
491         // Get raw signature
492         final String signature = method.getSignature();
493         // Get array of strings containing the argument types
494         final String[] args = Utility.methodSignatureArgumentTypes(signature, false);
495         // Get return type string
496         final String type = Utility.methodSignatureReturnType(signature, false);
497         // Get method name
498         final String name = method.getName();
499         final String htmlName = Class2HTML.toHTML(name);
500         // Get method's access flags
501         String access = Utility.accessToString(method.getAccessFlags());
502         access = Utility.replace(access, " ", "&nbsp;");
503         // Get the method's attributes, the Code Attribute in particular
504         final Attribute[] attributes = method.getAttributes();
505         printWriter.print("<P><B><FONT COLOR=\"#FF0000\">" + access + "</FONT>&nbsp;<A NAME=method" + methodNumber + ">" + Class2HTML.referenceType(type)
506             + "</A>&nbsp<A HREF=\"" + className + "_methods.html#method" + methodNumber + "\" TARGET=Methods>" + htmlName + "</A>(");
507         for (int i = 0; i < args.length; i++) {
508             printWriter.print(Class2HTML.referenceType(args[i]));
509             if (i < args.length - 1) {
510                 printWriter.print(",&nbsp;");
511             }
512         }
513         printWriter.println(")</B></P>");
514         Code c = null;
515         byte[] code = null;
516         if (attributes.length > 0) {
517             printWriter.print("<H4>Attributes</H4><UL>\n");
518             for (int i = 0; i < attributes.length; i++) {
519                 byte tag = attributes[i].getTag();
520                 if (tag != Const.ATTR_UNKNOWN) {
521                     printWriter.print("<LI><A HREF=\"" + className + "_attributes.html#method" + methodNumber + "@" + i + "\" TARGET=Attributes>"
522                         + Const.getAttributeName(tag) + "</A></LI>\n");
523                 } else {
524                     printWriter.print("<LI>" + attributes[i] + "</LI>");
525                 }
526                 if (tag == Const.ATTR_CODE) {
527                     c = (Code) attributes[i];
528                     final Attribute[] attributes2 = c.getAttributes();
529                     code = c.getCode();
530                     printWriter.print("<UL>");
531                     for (int j = 0; j < attributes2.length; j++) {
532                         tag = attributes2[j].getTag();
533                         printWriter.print("<LI><A HREF=\"" + className + "_attributes.html#method" + methodNumber + "@" + i + "@" + j
534                             + "\" TARGET=Attributes>" + Const.getAttributeName(tag) + "</A></LI>\n");
535                     }
536                     printWriter.print("</UL>");
537                 }
538             }
539             printWriter.println("</UL>");
540         }
541         if (code != null) { // No code, an abstract method, for example
542             // System.out.println(name + "\n" + Utility.codeToString(code, constantPool, 0, -1));
543             // Print the byte code
544             try (ByteSequence stream = new ByteSequence(code)) {
545                 stream.mark(stream.available());
546                 findGotos(stream, c);
547                 stream.reset();
548                 printWriter.println("<TABLE BORDER=0><TR><TH ALIGN=LEFT>Byte<BR>offset</TH><TH ALIGN=LEFT>Instruction</TH><TH ALIGN=LEFT>Argument</TH>");
549                 while (stream.available() > 0) {
550                     final int offset = stream.getIndex();
551                     final String str = codeToHTML(stream, methodNumber);
552                     String anchor = "";
553                     /*
554                      * Sets an anchor mark if this line is targetted by a goto, jsr, etc. Defining an anchor for every line is very
555                      * inefficient!
556                      */
557                     if (gotoSet.get(offset)) {
558                         anchor = "<A NAME=code" + methodNumber + "@" + offset + "></A>";
559                     }
560                     final String anchor2;
561                     if (stream.getIndex() == code.length) {
562                         anchor2 = "<A NAME=code" + methodNumber + "@" + code.length + ">" + offset + "</A>";
563                     } else {
564                         anchor2 = "" + offset;
565                     }
566                     printWriter.println("<TR VALIGN=TOP><TD>" + anchor2 + "</TD><TD>" + anchor + str + "</TR>");
567                 }
568             }
569             // Mark last line, may be targetted from Attributes window
570             printWriter.println("<TR><TD> </A></TD></TR>");
571             printWriter.println("</TABLE>");
572         }
573     }
574 }