1 /* 2 * Copyright (c) 1997, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.javadoc.internal.doclets.formats.html; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.Set; 31 import java.util.TreeSet; 32 import java.util.stream.Collectors; 33 34 import javax.lang.model.element.Element; 35 import javax.lang.model.element.ExecutableElement; 36 import javax.lang.model.element.Modifier; 37 import javax.lang.model.element.TypeElement; 38 import javax.lang.model.element.TypeParameterElement; 39 import javax.lang.model.type.TypeMirror; 40 41 import com.sun.source.doctree.DocTree; 42 43 import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder; 44 import jdk.javadoc.internal.doclets.formats.html.markup.Entity; 45 import jdk.javadoc.internal.doclets.formats.html.markup.HtmlStyle; 46 import jdk.javadoc.internal.doclets.formats.html.markup.HtmlTag; 47 import jdk.javadoc.internal.doclets.formats.html.markup.HtmlTree; 48 import jdk.javadoc.internal.doclets.formats.html.markup.Links; 49 import jdk.javadoc.internal.doclets.formats.html.markup.StringContent; 50 import jdk.javadoc.internal.doclets.formats.html.markup.Table; 51 import jdk.javadoc.internal.doclets.formats.html.markup.TableHeader; 52 import jdk.javadoc.internal.doclets.toolkit.Content; 53 import jdk.javadoc.internal.doclets.toolkit.MemberSummaryWriter; 54 import jdk.javadoc.internal.doclets.toolkit.Resources; 55 import jdk.javadoc.internal.doclets.toolkit.taglets.DeprecatedTaglet; 56 import jdk.javadoc.internal.doclets.toolkit.util.DocletConstants; 57 import jdk.javadoc.internal.doclets.toolkit.util.Utils; 58 59 import static javax.lang.model.element.Modifier.ABSTRACT; 60 import static javax.lang.model.element.Modifier.NATIVE; 61 import static javax.lang.model.element.Modifier.PUBLIC; 62 import static javax.lang.model.element.Modifier.STRICTFP; 63 import static javax.lang.model.element.Modifier.SYNCHRONIZED; 64 65 /** 66 * The base class for member writers. 67 * 68 * <p><b>This is NOT part of any supported API. 69 * If you write code that depends on this, you do so at your own risk. 70 * This code and its internal interfaces are subject to change or 71 * deletion without notice.</b> 72 * 73 * @author Robert Field 74 * @author Atul M Dambalkar 75 * @author Jamie Ho (Re-write) 76 * @author Bhavesh Patel (Modified) 77 */ 78 public abstract class AbstractMemberWriter implements MemberSummaryWriter { 79 80 protected final HtmlConfiguration configuration; 81 protected final Utils utils; 82 protected final SubWriterHolderWriter writer; 83 protected final Contents contents; 84 protected final Resources resources; 85 protected final Links links; 86 87 protected final TypeElement typeElement; 88 public final boolean nodepr; 89 90 protected boolean printedSummaryHeader = false; 91 92 public AbstractMemberWriter(SubWriterHolderWriter writer, TypeElement typeElement) { 93 this.configuration = writer.configuration; 94 this.writer = writer; 95 this.nodepr = configuration.nodeprecated; 96 this.typeElement = typeElement; 97 this.utils = configuration.utils; 98 this.contents = configuration.contents; 99 this.resources = configuration.resources; 100 this.links = writer.links; 101 } 102 103 public AbstractMemberWriter(SubWriterHolderWriter writer) { 104 this(writer, null); 105 } 106 107 /*** abstracts ***/ 108 109 /** 110 * Add the summary label for the member. 111 * 112 * @param memberTree the content tree to which the label will be added 113 */ 114 public abstract void addSummaryLabel(Content memberTree); 115 116 /** 117 * Get the summary for the member summary table. 118 * 119 * @return a string for the table summary 120 */ 121 private String getTableSummaryX() { return null; } 122 123 /** 124 * Get the summary table header for the member. 125 * 126 * @param member the member to be documented 127 * @return the summary table header 128 */ 129 public abstract TableHeader getSummaryTableHeader(Element member); 130 131 private Table summaryTable; 132 133 private Table getSummaryTable() { 134 if (summaryTable == null) { 135 summaryTable = createSummaryTable(); 136 } 137 return summaryTable; 138 } 139 140 /** 141 * Create the summary table for this element. 142 * The table should be created and initialized if needed, and configured 143 * so that it is ready to add content with {@link Table#addRow(Content[])} 144 * and similar methods. 145 * 146 * @return the summary table 147 */ 148 protected abstract Table createSummaryTable(); 149 150 151 152 /** 153 * Add inherited summary label for the member. 154 * 155 * @param typeElement the TypeElement to which to link to 156 * @param inheritedTree the content tree to which the inherited summary label will be added 157 */ 158 public abstract void addInheritedSummaryLabel(TypeElement typeElement, Content inheritedTree); 159 160 /** 161 * Add the anchor for the summary section of the member. 162 * 163 * @param typeElement the TypeElement to be documented 164 * @param memberTree the content tree to which the summary anchor will be added 165 */ 166 public abstract void addSummaryAnchor(TypeElement typeElement, Content memberTree); 167 168 /** 169 * Add the anchor for the inherited summary section of the member. 170 * 171 * @param typeElement the TypeElement to be documented 172 * @param inheritedTree the content tree to which the inherited summary anchor will be added 173 */ 174 public abstract void addInheritedSummaryAnchor(TypeElement typeElement, Content inheritedTree); 175 176 /** 177 * Add the summary type for the member. 178 * 179 * @param member the member to be documented 180 * @param tdSummaryType the content tree to which the type will be added 181 */ 182 protected abstract void addSummaryType(Element member, Content tdSummaryType); 183 184 /** 185 * Add the summary link for the member. 186 * 187 * @param typeElement the TypeElement to be documented 188 * @param member the member to be documented 189 * @param tdSummary the content tree to which the link will be added 190 */ 191 protected void addSummaryLink(TypeElement typeElement, Element member, Content tdSummary) { 192 addSummaryLink(LinkInfoImpl.Kind.MEMBER, typeElement, member, tdSummary); 193 } 194 195 /** 196 * Add the summary link for the member. 197 * 198 * @param context the id of the context where the link will be printed 199 * @param typeElement the TypeElement to be documented 200 * @param member the member to be documented 201 * @param tdSummary the content tree to which the summary link will be added 202 */ 203 protected abstract void addSummaryLink(LinkInfoImpl.Kind context, 204 TypeElement typeElement, Element member, Content tdSummary); 205 206 /** 207 * Add the inherited summary link for the member. 208 * 209 * @param typeElement the TypeElement to be documented 210 * @param member the member to be documented 211 * @param linksTree the content tree to which the inherited summary link will be added 212 */ 213 protected abstract void addInheritedSummaryLink(TypeElement typeElement, 214 Element member, Content linksTree); 215 216 /** 217 * Get the deprecated link. 218 * 219 * @param member the member being linked to 220 * @return a content tree representing the link 221 */ 222 protected abstract Content getDeprecatedLink(Element member); 223 224 protected CharSequence makeSpace(int len) { 225 if (len <= 0) { 226 return ""; 227 } 228 StringBuilder sb = new StringBuilder(len); 229 for (int i = 0; i < len; i++) { 230 sb.append(' '); 231 } 232 return sb; 233 } 234 235 /** 236 * Add the modifier and type for the member in the member summary. 237 * 238 * @param member the member to add the type for 239 * @param type the type to add 240 * @param tdSummaryType the content tree to which the modified and type will be added 241 */ 242 protected void addModifierAndType(Element member, TypeMirror type, 243 Content tdSummaryType) { 244 HtmlTree code = new HtmlTree(HtmlTag.CODE); 245 addModifier(member, code); 246 if (type == null) { 247 code.add(utils.isClass(member) ? "class" : "interface"); 248 code.add(Entity.NO_BREAK_SPACE); 249 } else { 250 List<? extends TypeParameterElement> list = utils.isExecutableElement(member) 251 ? ((ExecutableElement)member).getTypeParameters() 252 : null; 253 if (list != null && !list.isEmpty()) { 254 Content typeParameters = ((AbstractExecutableMemberWriter) this) 255 .getTypeParameters((ExecutableElement)member); 256 code.add(typeParameters); 257 //Code to avoid ugly wrapping in member summary table. 258 if (typeParameters.charCount() > 10) { 259 code.add(new HtmlTree(HtmlTag.BR)); 260 } else { 261 code.add(Entity.NO_BREAK_SPACE); 262 } 263 code.add( 264 writer.getLink(new LinkInfoImpl(configuration, 265 LinkInfoImpl.Kind.SUMMARY_RETURN_TYPE, type))); 266 } else { 267 code.add( 268 writer.getLink(new LinkInfoImpl(configuration, 269 LinkInfoImpl.Kind.SUMMARY_RETURN_TYPE, type))); 270 } 271 272 } 273 tdSummaryType.add(code); 274 } 275 276 /** 277 * Add the modifier for the member. 278 * 279 * @param member the member to add the type for 280 * @param code the content tree to which the modified will be added 281 */ 282 private void addModifier(Element member, Content code) { 283 if (utils.isProtected(member)) { 284 code.add("protected "); 285 } else if (utils.isPrivate(member)) { 286 code.add("private "); 287 } else if (!utils.isPublic(member)) { // Package private 288 code.add(resources.getText("doclet.Package_private")); 289 code.add(" "); 290 } 291 boolean isAnnotatedTypeElement = utils.isAnnotationType(member.getEnclosingElement()); 292 if (!isAnnotatedTypeElement && utils.isMethod(member)) { 293 if (!utils.isInterface(member.getEnclosingElement()) && utils.isAbstract(member)) { 294 code.add("abstract "); 295 } 296 if (utils.isDefault(member)) { 297 code.add("default "); 298 } 299 } 300 if (utils.isStatic(member)) { 301 code.add("static "); 302 } 303 } 304 305 /** 306 * Add the deprecated information for the given member. 307 * 308 * @param member the member being documented. 309 * @param contentTree the content tree to which the deprecated information will be added. 310 */ 311 protected void addDeprecatedInfo(Element member, Content contentTree) { 312 Content output = (new DeprecatedTaglet()).getTagletOutput(member, 313 writer.getTagletWriterInstance(false)); 314 if (!output.isEmpty()) { 315 Content deprecatedContent = output; 316 Content div = HtmlTree.DIV(HtmlStyle.deprecationBlock, deprecatedContent); 317 contentTree.add(div); 318 } 319 } 320 321 /** 322 * Add the comment for the given member. 323 * 324 * @param member the member being documented. 325 * @param htmltree the content tree to which the comment will be added. 326 */ 327 protected void addComment(Element member, Content htmltree) { 328 if (!utils.getFullBody(member).isEmpty()) { 329 writer.addInlineComment(member, htmltree); 330 } 331 } 332 333 protected String name(Element member) { 334 return utils.getSimpleName(member); 335 } 336 337 /** 338 * Return true if the given <code>ProgramElement</code> is inherited 339 * by the class that is being documented. 340 * 341 * @param ped The <code>ProgramElement</code> being checked. 342 * return true if the <code>ProgramElement</code> is being inherited and 343 * false otherwise. 344 *@return true if inherited 345 */ 346 protected boolean isInherited(Element ped){ 347 return (!utils.isPrivate(ped) && 348 (!utils.isPackagePrivate(ped) || 349 ped.getEnclosingElement().equals(ped.getEnclosingElement()))); 350 } 351 352 /** 353 * Add use information to the documentation tree. 354 * 355 * @param mems list of program elements for which the use information will be added 356 * @param heading the section heading 357 * @param contentTree the content tree to which the use information will be added 358 */ 359 protected void addUseInfo(List<? extends Element> mems, Content heading, Content contentTree) { 360 if (mems == null || mems.isEmpty()) { 361 return; 362 } 363 List<? extends Element> members = mems; 364 boolean printedUseTableHeader = false; 365 if (members.size() > 0) { 366 Table useTable = new Table(HtmlStyle.useSummary) 367 .setCaption(heading) 368 .setRowScopeColumn(1) 369 .setColumnStyles(HtmlStyle.colFirst, HtmlStyle.colSecond, HtmlStyle.colLast); 370 for (Element element : members) { 371 TypeElement te = (typeElement == null) 372 ? utils.getEnclosingTypeElement(element) 373 : typeElement; 374 if (!printedUseTableHeader) { 375 useTable.setHeader(getSummaryTableHeader(element)); 376 printedUseTableHeader = true; 377 } 378 Content summaryType = new ContentBuilder(); 379 addSummaryType(element, summaryType); 380 Content typeContent = new ContentBuilder(); 381 if (te != null 382 && !utils.isConstructor(element) 383 && !utils.isClass(element) 384 && !utils.isInterface(element) 385 && !utils.isAnnotationType(element)) { 386 HtmlTree name = new HtmlTree(HtmlTag.SPAN); 387 name.setStyle(HtmlStyle.typeNameLabel); 388 name.add(name(te) + "."); 389 typeContent.add(name); 390 } 391 addSummaryLink(utils.isClass(element) || utils.isInterface(element) 392 ? LinkInfoImpl.Kind.CLASS_USE 393 : LinkInfoImpl.Kind.MEMBER, 394 te, element, typeContent); 395 Content desc = new ContentBuilder(); 396 writer.addSummaryLinkComment(this, element, desc); 397 useTable.addRow(summaryType, typeContent, desc); 398 } 399 contentTree.add(useTable.toContent()); 400 } 401 } 402 403 protected void serialWarning(Element e, String key, String a1, String a2) { 404 if (configuration.serialwarn) { 405 configuration.messages.warning(e, key, a1, a2); 406 } 407 } 408 409 /** 410 * Add the member summary for the given class. 411 * 412 * @param tElement the class that is being documented 413 * @param member the member being documented 414 * @param firstSentenceTags the first sentence tags to be added to the summary 415 */ 416 @Override 417 public void addMemberSummary(TypeElement tElement, Element member, 418 List<? extends DocTree> firstSentenceTags) { 419 if (tElement != typeElement) { 420 throw new IllegalStateException(); 421 } 422 Table table = getSummaryTable(); 423 List<Content> rowContents = new ArrayList<>(); 424 Content summaryType = new ContentBuilder(); 425 addSummaryType(member, summaryType); 426 if (!summaryType.isEmpty()) 427 rowContents.add(summaryType); 428 Content summaryLink = new ContentBuilder(); 429 addSummaryLink(tElement, member, summaryLink); 430 rowContents.add(summaryLink); 431 Content desc = new ContentBuilder(); 432 writer.addSummaryLinkComment(this, member, firstSentenceTags, desc); 433 rowContents.add(desc); 434 table.addRow(member, rowContents); 435 } 436 437 /** 438 * Add inherited member summary for the given class and member. 439 * 440 * @param tElement the class the inherited member belongs to 441 * @param nestedClass the inherited member that is summarized 442 * @param isFirst true if this is the first member in the list 443 * @param isLast true if this is the last member in the list 444 * @param linksTree the content tree to which the summary will be added 445 */ 446 @Override 447 public void addInheritedMemberSummary(TypeElement tElement, 448 Element nestedClass, boolean isFirst, boolean isLast, 449 Content linksTree) { 450 writer.addInheritedMemberSummary(this, tElement, nestedClass, isFirst, 451 linksTree); 452 } 453 454 /** 455 * Get the inherited summary header for the given class. 456 * 457 * @param tElement the class the inherited member belongs to 458 * @return a content tree for the inherited summary header 459 */ 460 @Override 461 public Content getInheritedSummaryHeader(TypeElement tElement) { 462 Content inheritedTree = writer.getMemberInheritedTree(); 463 writer.addInheritedSummaryHeader(this, tElement, inheritedTree); 464 return inheritedTree; 465 } 466 467 /** 468 * Get the inherited summary links tree. 469 * 470 * @return a content tree for the inherited summary links 471 */ 472 @Override 473 public Content getInheritedSummaryLinksTree() { 474 return new HtmlTree(HtmlTag.CODE); 475 } 476 477 /** 478 * Get the summary table tree for the given class. 479 * 480 * @param tElement the class for which the summary table is generated 481 * @return a content tree for the summary table 482 */ 483 @Override 484 public Content getSummaryTableTree(TypeElement tElement) { 485 if (tElement != typeElement) { 486 throw new IllegalStateException(); 487 } 488 Table table = getSummaryTable(); 489 if (table.needsScript()) { 490 writer.getMainBodyScript().append(table.getScript()); 491 } 492 return table.toContent(); 493 } 494 495 /** 496 * Get the member tree to be documented. 497 * 498 * @param memberTree the content tree of member to be documented 499 * @return a content tree that will be added to the class documentation 500 */ 501 @Override 502 public Content getMemberTree(Content memberTree) { 503 return writer.getMemberTree(memberTree); 504 } 505 506 /** 507 * Get the member tree to be documented. 508 * 509 * @param memberTree the content tree of member to be documented 510 * @param isLastContent true if the content to be added is the last content 511 * @return a content tree that will be added to the class documentation 512 */ 513 public Content getMemberTree(Content memberTree, boolean isLastContent) { 514 if (isLastContent) 515 return HtmlTree.LI(HtmlStyle.blockListLast, memberTree); 516 else 517 return HtmlTree.LI(HtmlStyle.blockList, memberTree); 518 } 519 520 /** 521 * A HtmlTree builder for member signatures. 522 */ 523 class MemberSignature { 524 525 // The content builder 526 private Content content = new ContentBuilder(); 527 528 // Position of last line separator. 529 private int lastNewLine = 0; 530 531 // Threshold for length of type parameters before switching from inline to block representation. 532 private final static int TYPE_PARAMS_MAX_INLINE_LENGTH = 50; 533 534 // Threshold for combined length of modifiers, type params and return type before breaking 535 // it up with a line break before the return type. 536 private final static int RETURN_TYPE_MAX_LINE_LENGTH = 50; 537 538 /** 539 * Create a new member signature tree. 540 * 541 * @param element The element for which to create a signature. 542 */ 543 MemberSignature(Element element) { 544 Content annotationInfo = writer.getAnnotationInfo(element.getAnnotationMirrors(), true); 545 if (!annotationInfo.isEmpty()) { 546 content.add(HtmlTree.SPAN(HtmlStyle.annotations, annotationInfo)); 547 lastNewLine = content.charCount(); 548 } 549 addModifiers(element); 550 } 551 552 /** 553 * Add the modifier for the member. The modifiers are ordered as specified 554 * by <em>The Java Language Specification</em>. 555 * 556 * @param member the member for which the modifier will be added. 557 */ 558 void addModifiers(Element member) { 559 Set<Modifier> set = new TreeSet<>(member.getModifiers()); 560 561 // remove the ones we really don't need 562 set.remove(NATIVE); 563 set.remove(SYNCHRONIZED); 564 set.remove(STRICTFP); 565 566 // According to JLS, we should not be showing public modifier for 567 // interface methods. 568 if ((utils.isField(member) || utils.isMethod(member)) 569 && ((writer instanceof ClassWriterImpl 570 && utils.isInterface(((ClassWriterImpl) writer).getTypeElement()) || 571 writer instanceof AnnotationTypeWriterImpl) )) { 572 // Remove the implicit abstract and public modifiers 573 if (utils.isMethod(member) && 574 (utils.isInterface(member.getEnclosingElement()) || 575 utils.isAnnotationType(member.getEnclosingElement()))) { 576 set.remove(ABSTRACT); 577 set.remove(PUBLIC); 578 } 579 if (!utils.isMethod(member)) { 580 set.remove(PUBLIC); 581 } 582 } 583 if (!set.isEmpty()) { 584 String mods = set.stream().map(Modifier::toString).collect(Collectors.joining(" ")); 585 content.add(HtmlTree.SPAN(HtmlStyle.modifiers, new StringContent(mods))); 586 content.add(Entity.NO_BREAK_SPACE); 587 } 588 } 589 590 /** 591 * Add the type parameters and return type for an executable member. 592 * 593 * @param typeParameters the content tree containing the type parameters to add. 594 * @param returnType the content tree containing the return type to add. 595 */ 596 void addTypeParametersAndReturnType(Content typeParameters, Content returnType) { 597 598 if (!typeParameters.isEmpty()) { 599 // Apply different wrapping strategies for type parameters 600 // depending of combined length of type parameters and return type. 601 int typeParamLength = typeParameters.charCount(); 602 603 if (typeParamLength >= TYPE_PARAMS_MAX_INLINE_LENGTH) { 604 content.add(HtmlTree.SPAN(HtmlStyle.typeParametersLong, typeParameters)); 605 } else { 606 content.add(HtmlTree.SPAN(HtmlStyle.typeParameters, typeParameters)); 607 } 608 609 // count below includes modifiers plus type params added above 610 if (charCountFromNewline() + returnType.charCount()> RETURN_TYPE_MAX_LINE_LENGTH) { 611 content.add(DocletConstants.NL); 612 lastNewLine = content.charCount(); 613 } else { 614 content.add(Entity.NO_BREAK_SPACE); 615 } 616 } 617 618 content.add(HtmlTree.SPAN(HtmlStyle.returnType, returnType)); 619 content.add(Entity.NO_BREAK_SPACE); 620 } 621 622 /** 623 * Add the type information for a non-executable member. 624 * 625 * @param type the type of the member. 626 */ 627 void addType(TypeMirror type) { 628 Content typeLink = writer.getLink(new LinkInfoImpl(configuration, LinkInfoImpl.Kind.MEMBER, type)); 629 content.add(HtmlTree.SPAN(HtmlStyle.returnType, typeLink)); 630 content.add(Entity.NO_BREAK_SPACE); 631 } 632 633 /** 634 * Add the name of a member. 635 * 636 * @param element the member being documented. 637 */ 638 void addName(Element element) { 639 HtmlTree nameSpan = new HtmlTree(HtmlTag.SPAN); 640 nameSpan.setStyle(HtmlStyle.memberName); 641 if (configuration.linksource) { 642 Content name = new StringContent(name(element)); 643 writer.addSrcLink(element, name, nameSpan); 644 } else { 645 nameSpan.add(name(element)); 646 } 647 content.add(nameSpan); 648 } 649 650 /** 651 * Add the parameter and exception information of an executable member. 652 * 653 * @param paramTree the content tree containing the parameter information. 654 * @param exceptionTree the content tree containing the exception information. 655 */ 656 void addParametersAndExceptions(Content paramTree, Content exceptionTree) { 657 int indentSize = charCountFromNewline(); 658 659 if (paramTree.isEmpty()) { 660 content.add("()"); 661 } else { 662 content.add(Entity.ZERO_WIDTH_SPACE); 663 content.add("("); 664 content.add(HtmlTree.SPAN(HtmlStyle.arguments, paramTree)); 665 paramTree.add(")"); 666 } 667 668 if (!exceptionTree.isEmpty()) { 669 CharSequence indent = makeSpace(indentSize + 1 - 7); 670 content.add(DocletConstants.NL); 671 content.add(indent); 672 content.add("throws "); 673 content.add(HtmlTree.SPAN(HtmlStyle.exceptions, exceptionTree)); 674 } 675 } 676 677 /** 678 * Return the number of characters of plain text content since the last newline character. 679 * 680 * @return the number of plain text characters since the last newline character 681 */ 682 int charCountFromNewline() { 683 return content.charCount() - lastNewLine; 684 } 685 686 /** 687 * Return a HTML tree containing the member signature. 688 * 689 * @return the member signature 690 */ 691 Content toContent() { 692 return HtmlTree.DIV(HtmlStyle.memberSignature, content); 693 } 694 695 } 696 } --- EOF ---