/*******************************************************************************
* Copyright (c) 2000, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.formatter;
import org.eclipse.jdt.internal.compiler.parser.Scanner;
import org.eclipse.jdt.internal.formatter.comment.IJavaDocTagConstants;
/**
* Represents a block in a {@link FormatJavadoc} which might be a <b>description</b> or a <b>tag</b>
* (see{@link #isDescription()}). </p>
* <p>
* The block might have a tag, a reference and nodes (see {@link FormatJavadocNode}. Each of these
* elements might be present or not, but at least one of them is.
* </p>
*/
public class FormatJavadocBlock extends FormatJavadocNode implements IJavaDocTagConstants {
// flags
final static int INLINED= 0x0001;
final static int FIRST= 0x0002;
final static int ON_HEADER_LINE= 0x0004;
final static int TEXT_ON_TAG_LINE= 0x0008;
final static int ONE_LINE_TAG= 0x0010;
final static int PARAM_TAG= 0x0020;
final static int IN_PARAM_TAG= 0x0040;
final static int IN_DESCRIPTION= 0x0080;
final static int IMMUTABLE= 0x0100;
// constants
final static int MAX_TAG_HIERARCHY= 10;
private int tagValue= NO_TAG_VALUE;
int tagEnd;
FormatJavadocReference reference;
FormatJavadocNode[] nodes;
int nodesPtr= -1;
int flags= 0;
public FormatJavadocBlock(int start, int end, int line, int value) {
super(start, end, line);
this.tagValue= value;
this.tagEnd= end;
switch (value) {
case TAG_PARAM_VALUE:
// TODO why are following tags considered like @param by the formatter?
case TAG_SERIAL_FIELD_VALUE:
case TAG_THROWS_VALUE:
case TAG_EXCEPTION_VALUE:
this.flags|= PARAM_TAG;
break;
case TAG_CODE_VALUE:
case TAG_LITERAL_VALUE:
this.flags|= IMMUTABLE;
break;
}
}
private void addNode(FormatJavadocNode node) {
// Initialize or resize array if necessary
if (++this.nodesPtr == 0) {
this.nodes= new FormatJavadocNode[DEFAULT_ARRAY_SIZE];
} else if (this.nodesPtr >= this.nodes.length) {
System.arraycopy(
this.nodes, 0,
this.nodes= new FormatJavadocNode[this.nodes.length + INCREMENT_ARRAY_SIZE], 0,
this.nodesPtr);
}
// Store the node
this.nodes[this.nodesPtr]= node;
this.sourceEnd= node.sourceEnd;
}
void addBlock(FormatJavadocBlock block, int htmlLevel) {
if (this.nodes != null) {
FormatJavadocText[] textHierarchy= getTextHierarchy(block, htmlLevel);
if (textHierarchy != null) {
FormatJavadocText lastText= textHierarchy[htmlLevel];
if (lastText != null) {
lastText.appendNode(block);
for (int i= htmlLevel - 1; i >= 0; i--) {
textHierarchy[i].sourceEnd= block.sourceEnd;
}
this.sourceEnd= block.sourceEnd;
if (isParamTag()) {
block.flags|= IN_PARAM_TAG;
} else if (isDescription()) {
block.flags|= IN_DESCRIPTION;
}
block.flags|= INLINED;
return;
}
}
}
addNode(block);
if (isParamTag()) {
block.flags|= IN_PARAM_TAG;
} else if (isDescription()) {
block.flags|= IN_DESCRIPTION;
}
block.flags|= INLINED;
}
void addText(FormatJavadocText text) {
if (this.nodes != null) {
FormatJavadocText[] textHierarchy= getTextHierarchy(text, text.depth);
if (textHierarchy != null) {
FormatJavadocText lastText= textHierarchy[text.depth];
if (lastText != null) {
lastText.appendText(text);
for (int i= text.depth - 1; i >= 0; i--) {
textHierarchy[i].sourceEnd= text.sourceEnd;
}
this.sourceEnd= text.sourceEnd;
return;
}
if (text.depth > 0) {
FormatJavadocText parentText= textHierarchy[text.depth - 1];
if (parentText != null) {
parentText.appendText(text);
for (int i= text.depth - 2; i >= 0; i--) {
textHierarchy[i].sourceEnd= text.sourceEnd;
}
this.sourceEnd= text.sourceEnd;
return;
}
}
}
}
if (text.isHtmlTag()) {
switch (text.getHtmlTagID()) {
case JAVADOC_CODE_TAGS_ID:
text.linesBefore= this.nodesPtr == -1 ? 0 : 2;
break;
case JAVADOC_SEPARATOR_TAGS_ID:
text.linesBefore= 1;
break;
// case JAVADOC_BREAK_TAGS_ID:
// if (this.nodesPtr >= 0) text.linesBefore = 1;
}
}
addNode(text);
if (isImmutable()) {
text.immutable= true;
}
}
void clean() {
int length= this.nodes == null ? 0 : this.nodes.length;
if (this.nodesPtr != (length - 1)) {
System.arraycopy(this.nodes, 0, this.nodes= new FormatJavadocNode[this.nodesPtr + 1], 0, this.nodesPtr + 1);
}
for (int i= 0; i <= this.nodesPtr; i++) {
this.nodes[i].clean();
}
}
FormatJavadocNode getLastNode() {
if (this.nodes != null) {
return this.nodes[this.nodesPtr];
}
return null;
}
/*
* Return the text hierarchy for the given node
*/
FormatJavadocText[] getTextHierarchy(FormatJavadocNode node, int htmlDepth) {
if (this.nodes == null)
return null;
FormatJavadocText[] textHierarchy= null;
int ptr= 0;
FormatJavadocText text= node.isText() ? (FormatJavadocText)node : null;
FormatJavadocNode lastNode= this.nodes[this.nodesPtr];
while (lastNode.isText()) {
FormatJavadocText lastText= (FormatJavadocText)lastNode;
int lastTagCategory= lastText.getHtmlTagID();
boolean lastSingleTag= lastTagCategory <= JAVADOC_SINGLE_TAGS_ID;
boolean lastTextCanHaveChildren= lastText.isHtmlTag() && !lastText.isClosingHtmlTag() && !lastSingleTag;
if (lastText.depth == htmlDepth || // found same html tag level => use it
lastText.htmlNodesPtr == -1) { // no more sub-levels => add one
// Text breakage
if (lastText.isHtmlTag() && text != null) {
// Set some lines before if previous was specific html tag
// The added text is concerned if the parent has no child yet or is top level and closing html tag
boolean setLinesBefore= lastText.separatorsPtr == -1 || (ptr == 0 && lastText.isClosingHtmlTag());
if (!setLinesBefore && ptr > 0 && lastText.isClosingHtmlTag()) {
// for non-top level closing html tag, text is concerned only if no new text has been written after
FormatJavadocText parentText= textHierarchy[ptr - 1];
int textStart= (int)parentText.separators[parentText.separatorsPtr];
if (textStart < lastText.sourceStart) {
setLinesBefore= true;
}
}
if (setLinesBefore) {
switch (lastText.getHtmlTagID()) {
case JAVADOC_CODE_TAGS_ID:
if (text.linesBefore < 2) {
text.linesBefore= 2;
}
break;
case JAVADOC_SEPARATOR_TAGS_ID:
case JAVADOC_SINGLE_BREAK_TAG_ID:
if (text.linesBefore < 1) {
text.linesBefore= 1;
}
}
}
// If adding an html tag on same html tag, then close previous one and leave
if (text.isHtmlTag() && !text.isClosingHtmlTag() && text.getHtmlTagIndex() == lastText.getHtmlTagIndex() && !lastText.isClosingHtmlTag()) {
lastText.closeTag();
return textHierarchy;
}
}
// If we have a text after another text, keep the same level to append
if (lastTextCanHaveChildren || (htmlDepth == 0 && !lastText.isHtmlTag() && text != null && !text.isHtmlTag())) {
if (textHierarchy == null)
textHierarchy= new FormatJavadocText[htmlDepth + 1];
textHierarchy[ptr]= lastText;
return textHierarchy;
}
// Last text cannot have children, so return the built hierarchy
return textHierarchy;
}
if (textHierarchy == null)
textHierarchy= new FormatJavadocText[htmlDepth + 1];
textHierarchy[ptr++]= lastText;
lastNode= lastText.htmlNodes[lastText.htmlNodesPtr];
}
return textHierarchy;
}
/**
* Returns whether the text is on the same line of the tag or not.
*
* @return <code>true</code> if the text is on the same line than the tag, <code>false</code>
* otherwise.
*/
public boolean hasTextOnTagLine() {
return (this.flags & TEXT_ON_TAG_LINE) != 0;
}
/**
* Returns whether the block is the javadoc comment description or not. The description begins
* after the starting delimiter and continues until the tag section.
*
* @return <code>true</code> if the block is the javadoc description, <code>false</code>
* otherwise.
*/
public boolean isDescription() {
return this.tagValue == NO_TAG_VALUE;
}
/**
* Returns whether the block is the first block of the javadoc comment or not (independently of
* the fact it's a description or not).
*
* @return <code>true</code> if the block is the first of the javadoc comment,
* <code>false</code> otherwise.
*/
public boolean isFirst() {
return (this.flags & FIRST) != 0;
}
/**
* Returns whether the first block starts on the same line than the javadoc starting delimiter
* or not.
*
* @return <code>true</code> if the the first block starts on the same line than the javadoc
* starting delimiter, <code>false</code> otherwise.
*/
public boolean isHeaderLine() {
return (this.flags & ON_HEADER_LINE) != 0;
}
/**
* Returns whether the block is immutable or not.
* <p>
* Currently, only {@code} and {@literal} inline tags block are considered as immutable.
* </p>
*
* @return <code>true</code> if the block is immutable, <code>false</code> otherwise.
*/
public boolean isImmutable() {
return (this.flags & IMMUTABLE) == IMMUTABLE;
}
/**
* Returns whether the block is a description or inlined in a description.
*
* @see #isParamTag()
*
* @return <code>true</code> if the block is a description or inlined in a description,
* <code>false</code> otherwise.
*/
public boolean isInDescription() {
return this.tagValue == NO_TAG_VALUE || (this.flags & IN_DESCRIPTION) == IN_DESCRIPTION;
}
/**
* Returns whether the text is on the same line of the tag.
*
* @return <code>true</code> if the text is on the same line than the tag, <code>false</code>
* otherwise.
*/
public boolean isInlined() {
return (this.flags & INLINED) != 0;
}
/**
* Returns whether the block is a param tag or inlined in a param tag.
*
* @see #isParamTag()
*
* @return <code>true</code> if the block is a param tag or inlined in a param tag,
* <code>false</code> otherwise.
*/
public boolean isInParamTag() {
return (this.flags & (PARAM_TAG | IN_PARAM_TAG)) != 0;
}
/**
* Returns whether the the tag is on one line only.
*
* @return <code>true</code> if the tag is on one line only, <code>false</code> otherwise.
*/
public boolean isOneLineTag() {
return (this.flags & ONE_LINE_TAG) != 0;
}
/**
* Returns whether the block is a param tag or not. Note that this also includes
* @serialField, @throws and @exception tags.
*
* @return <code>true</code> if the block is a param tag, <code>false</code> otherwise.
*/
public boolean isParamTag() {
return (this.flags & PARAM_TAG) == PARAM_TAG;
}
void setHeaderLine(int javadocLineStart) {
if (javadocLineStart == this.lineStart) {
this.flags|= ON_HEADER_LINE;
}
for (int i= 0; i < this.nodesPtr; i++) {
this.nodes[i].setHeaderLine(javadocLineStart);
}
}
protected void toString(StringBuffer buffer) {
boolean inlined= (this.flags & INLINED) != 0;
if (inlined)
buffer.append(" {"); //$NON-NLS-1$
buffer.append('@');
if (this.tagValue == TAG_OTHERS_VALUE) {
buffer.append("others_tag"); //$NON-NLS-1$
} else {
buffer.append(TAG_NAMES[this.tagValue]);
}
super.toString(buffer);
if (this.reference == null) {
buffer.append('\n');
} else {
buffer.append(" ("); //$NON-NLS-1$
this.reference.toString(buffer);
buffer.append(")\n"); //$NON-NLS-1$
}
StringBuffer flagsBuffer= new StringBuffer();
if (isDescription()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("description"); //$NON-NLS-1$
}
if (isFirst()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("first"); //$NON-NLS-1$
}
if (isHeaderLine()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("header line"); //$NON-NLS-1$
}
if (isImmutable()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("immutable"); //$NON-NLS-1$
}
if (isInDescription()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("in description"); //$NON-NLS-1$
}
if (isInlined()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("inlined"); //$NON-NLS-1$
}
if (isInParamTag()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("in param tag"); //$NON-NLS-1$
}
if (isOneLineTag()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("one line tag"); //$NON-NLS-1$
}
if (isParamTag()) {
if (flagsBuffer.length() > 0)
flagsBuffer.append(',');
flagsBuffer.append("param tag"); //$NON-NLS-1$
}
if (flagsBuffer.length() > 0) {
if (inlined)
buffer.append('\t');
buffer.append(" flags: "); //$NON-NLS-1$
buffer.append(flagsBuffer);
buffer.append('\n');
}
if (this.nodesPtr > -1) {
for (int i= 0; i <= this.nodesPtr; i++) {
if (inlined)
buffer.append('\t');
this.nodes[i].toString(buffer);
}
}
}
public String toStringDebug(char[] source) {
StringBuffer buffer= new StringBuffer();
toStringDebug(buffer, source);
return buffer.toString();
}
public void toStringDebug(StringBuffer buffer, char[] source) {
if (this.tagValue > 0) {
buffer.append(source, this.sourceStart, this.tagEnd - this.sourceStart + 1);
buffer.append(' ');
}
if (this.reference != null) {
this.reference.toStringDebug(buffer, source);
}
for (int i= 0; i <= this.nodesPtr; i++) {
this.nodes[i].toStringDebug(buffer, source);
}
}
void update(Scanner scanner) {
int blockEnd= scanner.getLineNumber(this.sourceEnd);
if (blockEnd == this.lineStart) {
this.flags|= FormatJavadocBlock.ONE_LINE_TAG;
}
for (int i= 0; i <= this.nodesPtr; i++) {
if (!this.nodes[i].isText()) {
((FormatJavadocBlock)this.nodes[i]).update(scanner);
}
}
}
}