/*
* 03/21/2010
*
* Copyright (C) 2010 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is distributed under a modified BSD license. See the included
* RSTALanguageSupport.License.txt file for details.
*/
package org.fife.rsta.ac.java;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.fife.rsta.ac.java.buildpath.SourceLocation;
import org.fife.rsta.ac.java.classreader.ClassFile;
import org.fife.rsta.ac.java.rjc.ast.CompilationUnit;
/**
* Utility methods for Java completion.
*
* @author Robert Futrell
* @version 1.0
*/
public class Util {
/**
* Optional leading text for doc comment lines (except the first line) that
* should be removed if it exists.
*/
static final Pattern DOC_COMMENT_LINE_HEADER =
Pattern.compile("\\s*\\n\\s*\\*");//^\\s*\\*\\s*[/]?");
/**
* Pattern matching a link in a "@link" tag. This should match the
* following:
*
* <ul>
* <li>ClassName</li>
* <li>fully.qualified.ClassName</li>
* <li>#method</li>
* <li>#method(int, int)</li>
* <li>String#method</li>
* <li>String#method(params)</li>
* <li>fully.qualified.ClassName#method</li>
* <li>fully.qualified.ClassName#method(params)</li>
* </ul>
*/
static final Pattern LINK_TAG_MEMBER_PATTERN =
Pattern.compile("(?:\\w+\\.)*\\w+(?:#\\w+(?:\\([^\\)]*\\))?)?|" +
"#\\w+(?:\\([^\\)]*\\))?");
/**
* A cache of the last {@link CompilationUnit} read from some attached
* source on disk. This is cached because, in some scenarios, the method
* {@link #getCompilationUnitFromDisk(File, ClassFile)} will be called for
* the same class many times in a row (such as to get method parameter
* info for all methods in a single class).
*/
private static CompilationUnit lastCUFromDisk;
private static SourceLocation lastCUFileParam;
private static ClassFile lastCUClassFileParam;
/**
* Private constructor to prevent instantiation.
*/
private Util() {
}
private static final void appendDocCommentTail(StringBuffer sb,
StringBuffer tail) {
StringBuffer params = null;
StringBuffer returns = null;
StringBuffer throwsItems = null;
StringBuffer see = null;
StringBuffer seeTemp = null;
StringBuffer since = null;
StringBuffer author = null;
StringBuffer version = null;
StringBuffer unknowns = null;
boolean inParams = false, inThrows = false,
inReturns = false, inSeeAlso = false,
inSince = false, inAuthor = false,
inVersion = false, inUnknowns = false;
String[] st = tail.toString().split("[ \t\r\n\f]+");
String token = null;
int i = 0;
while (i<st.length && (token=st[i++])!=null) {
if ("@param".equals(token) && i<st.length) {
token = st[i++]; // Actual parameter
if (params==null) {
params = new StringBuffer("<b>Parameters:</b><p class='indented'>");
}
else {
params.append("<br>");
}
params.append("<b>").append(token).append("</b> ");
inSeeAlso=false;
inParams = true;
inReturns = false;
inThrows = false;
inSince = false;
inAuthor = false;
inVersion = false;
inUnknowns = false;
}
else if ("@return".equals(token) && i<st.length) {
if (returns==null) {
returns = new StringBuffer("<b>Returns:</b><p class='indented'>");
}
inSeeAlso=false;
inReturns = true;
inParams = false;
inThrows = false;
inSince = false;
inAuthor = false;
inVersion = false;
inUnknowns = false;
}
else if ("@see".equals(token) && i<st.length) {
if (see==null) {
see = new StringBuffer("<b>See Also:</b><p class='indented'>");
seeTemp = new StringBuffer();
}
else {
if (seeTemp.length()>0) {
String temp = seeTemp.substring(0, seeTemp.length()-1);
//syntax is exactly the same as link
appendLinkTagText(see, temp);
}
see.append("<br>");
seeTemp.setLength(0);
//see.append("<br>");
}
inSeeAlso = true;
inReturns = false;
inParams = false;
inThrows = false;
inSince = false;
inAuthor = false;
inVersion = false;
inUnknowns = false;
}
else if (("@throws".equals(token)) ||
("@exception".equals(token)) && i<st.length) {
token = st[i++]; // Actual throwable
if (throwsItems==null) {
throwsItems = new StringBuffer("<b>Throws:</b><p class='indented'>");
}
else {
throwsItems.append("<br>");
}
throwsItems.append("<b>").append(token).append("</b> ");
inSeeAlso = false;
inParams = false;
inReturns = false;
inThrows = true;
inSince = false;
inAuthor = false;
inVersion = false;
inUnknowns = false;
}
else if ("@since".equals(token) && i<st.length) {
if (since==null) {
since = new StringBuffer("<b>Since:</b><p class='indented'>");
}
inSeeAlso=false;
inReturns = false;
inParams = false;
inThrows = false;
inSince = true;
inAuthor = false;
inVersion = false;
inUnknowns = false;
}
else if ("@author".equals(token) && i<st.length) {
if (author==null) {
author = new StringBuffer("<b>Author:</b><p class='indented'>");
}
else {
author.append("<br>");
}
inSeeAlso=false;
inReturns = false;
inParams = false;
inThrows = false;
inSince = false;
inAuthor = true;
inVersion = false;
inUnknowns = false;
}
else if ("@version".equals(token) && i<st.length) {
if (version==null) {
version = new StringBuffer("<b>Version:</b><p class='indented'>");
}
else {
version.append("<br>");
}
inSeeAlso=false;
inReturns = false;
inParams = false;
inThrows = false;
inSince = false;
inAuthor = false;
inVersion = true;
inUnknowns = false;
}
else if (token.startsWith("@") && token.length()>1) {
if (unknowns==null) {
unknowns = new StringBuffer();
}
else {
unknowns.append("</p>");
}
unknowns.append("<b>").append(token).append("</b><p class='indented'>");
// Stop everything; unknown/unsupported tag
inSeeAlso = false;
inParams = false;
inReturns = false;
inThrows = false;
inSince = false;
inAuthor = false;
inVersion = false;
inUnknowns = true;
}
else if (inParams) {
params.append(token).append(' ');
}
else if (inReturns) {
returns.append(token).append(' ');
}
else if (inSeeAlso) {
//see.append(token).append(' ');
seeTemp.append(token).append(' ');
}
else if (inThrows) {
throwsItems.append(token).append(' ');
}
else if (inSince) {
since.append(token).append(' ');
}
else if (inAuthor) {
author.append(token).append(' ');
}
else if (inVersion) {
version.append(token).append(' ');
}
else if (inUnknowns) {
unknowns.append(token).append(' ');
}
}
sb.append("<p>");
if (params!=null) {
sb.append(params).append("</p>");
}
if (returns!=null) {
sb.append(returns).append("</p>");
}
if (throwsItems!=null) {
sb.append(throwsItems).append("</p>");
}
if (see!=null) {
if (seeTemp.length()>0) { // Last @see contents
String temp = seeTemp.substring(0, seeTemp.length()-1);
//syntax is exactly the same as link
appendLinkTagText(see, temp);
}
see.append("<br>");
sb.append(see).append("</p>");
}
if (author!=null) {
sb.append(author).append("</p>");
}
if (version!=null) {
sb.append(version).append("</p>");
}
if (since!=null) {
sb.append(since).append("</p>");
}
if (unknowns!=null) {
sb.append(unknowns).append("</p>");
}
}
/**
* Appends HTML representing a "link" or "linkplain" Javadoc element to
* a string buffer.
*
* @param appendTo The buffer to append to.
* @param linkContent The content of a "link" or "linkplain" item.
*/
private static final void appendLinkTagText(StringBuffer appendTo,
String linkContent) {
appendTo.append("<a href='");
linkContent = linkContent.trim(); // If "@link" and text on different lines
Matcher m = LINK_TAG_MEMBER_PATTERN.matcher(linkContent);
if (m.find() && m.start() == 0) {
//System.out.println("Match!!! - '" + m.group(0));
String match = m.group(0); // Prevents recalculation
String link = match;
// TODO: If this starts with '#', "link" must be prepended with
// class name.
String text = null;
// No link "text" after the link location - just use link location
if (match.length() == linkContent.length()) {
int pound = match.indexOf('#');
if (pound==0) { // Just a method or field in this class
text = match.substring(1);
}
else if (pound>0) { // Not -1
String prefix = match.substring(0, pound);
if ("java.lang.Object".equals(prefix)) {
text = match.substring(pound+1);
}
}
else { // Just use whole match (invalid link?)
// TODO: Could be just a class name. Find on classpath
text = match;
}
}
else { // match.length() < linkContent.length()
int offs = match.length();
// Will usually skip just a single space
while (offs<linkContent.length() &&
Character.isWhitespace(linkContent.charAt(offs))) {
offs++;
}
if (offs<linkContent.length()) {
text = linkContent.substring(offs);
}
}
// No "better" text for link found - just use match.
if (text==null) {
text = linkContent;//.substring(match.length());
}
// Replace the '#' sign, if any.
text = fixLinkText(text);
appendTo./*append("link://").*/append(link).append("'>").append(text);
}
else { // Malformed link tag
System.out.println("Unmatched linkContent: " + linkContent);
appendTo.append("'>").append(linkContent);
}
appendTo.append("</a>");
}
/**
* Converts a Java documentation comment to HTML.
* <pre>
* This is a
* pre block
*</pre>
* @param dc The documentation comment.
* @return An HTML version of the comment.
*/
public static final String docCommentToHtml(String dc) {
if (dc==null) {
return null;
}
if (dc.endsWith("*/")) {
dc = dc.substring(0, dc.length()-2);
}
// First, strip the line transitions. These always seem to be stripped
// first from Javadoc, even when in between <pre> and </pre> tags.
Matcher m = DOC_COMMENT_LINE_HEADER.matcher(dc);
dc = m.replaceAll("\n");
StringBuffer html = new StringBuffer(
"<html><style> .indented { margin-top: 0px; padding-left: 30pt; } </style><body>");
StringBuffer tailBuf = null;
BufferedReader r = new BufferedReader(new StringReader(dc));
try {
// Handle the first line (guaranteed to be at least 1 line).
String line = r.readLine().substring(3);
line = possiblyStripDocCommentTail(line);
int offs = 0;
while (offs<line.length() && Character.isWhitespace(line.charAt(offs))) {
offs++;
}
if (offs<line.length()) {
html.append(line.substring(offs));
}
boolean inPreBlock = isInPreBlock(line, false);
html.append(inPreBlock ? '\n' : ' ');
// Read all subsequent lines.
while ((line=r.readLine())!=null) {
line = possiblyStripDocCommentTail(line);
if (tailBuf!=null) {
tailBuf.append(line).append(' ');
}
else if (line.trim().startsWith("@")) {
tailBuf = new StringBuffer();
tailBuf.append(line).append(' ');
}
else {
html.append(line);
inPreBlock = isInPreBlock(line, inPreBlock);
html.append(inPreBlock ? '\n' : ' ');
}
}
} catch (IOException ioe) { // Never happens
ioe.printStackTrace();
}
html = fixDocComment(html); // Fix stuff like "{@code}"
if (tailBuf!=null) {
appendDocCommentTail(html, fixDocComment(tailBuf));
}
return html.toString();
}
public static String forXML(String aText){
final StringBuffer result = new StringBuffer();
final StringCharacterIterator iterator = new StringCharacterIterator(aText);
char character = iterator.current();
while (character != CharacterIterator.DONE ){
if (character == '<') {
result.append("<");
}
else if (character == '>') {
result.append(">");
}
else if (character == '\"') {
result.append(""");
}
else if (character == '\'') {
result.append("'");
}
else if (character == '&') {
result.append("&");
}
else {
//the char is not a special one
//add it to the result as is
result.append(character);
}
character = iterator.next();
}
return result.toString();
}
private static final StringBuffer fixDocComment(StringBuffer text) {
// Nothing to do.
int index = text.indexOf("{@");
if (index==-1) {
return text;
}
// TODO: In Java 5, replace "sb.append(sb2.substring(...))"
// calls with "sb.append(sb2, offs, len)".
StringBuffer sb = new StringBuffer();
int textOffs = 0;
do {
int closingBrace = indexOf('}', text, index+2);
if (closingBrace>-1) { // Should practically always be true
sb.append(text.substring(textOffs, index));
String content = text.substring(index+2, closingBrace);
index = textOffs = closingBrace + 1;
if (content.startsWith("code ")) {
sb.append("<code>").
append(forXML(content.substring(5))).
append("</code>");
}
else if (content.startsWith("link ")) {
sb.append("<code>");
appendLinkTagText(sb, content.substring(5));
sb.append("</code>");
}
else if (content.startsWith("linkplain ")) {
appendLinkTagText(sb, content.substring(10));
}
else if (content.startsWith("literal ")) {
// TODO: Should escape HTML-breaking chars, such as '>'.
sb.append(content.substring(8));
}
else { // Unhandled Javadoc tag
sb.append("<code>").append(content).append("</code>");
}
}
else {
break; // Unclosed javadoc tag - just bail
}
} while ((index=text.indexOf("{@", index))>-1);
if (textOffs<text.length()) {
sb.append(text.substring(textOffs));
}
return sb;
}
/**
* Tidies up a link's display text for use in a <a> tag.
*
* @param text The text (a class, method, or field signature).
* @return The display value for the signature.
*/
private static final String fixLinkText(String text) {
if (text.startsWith("#")) { // Method in the current class
return text.substring(1);
}
return text.replace('#', '.');
}
/**
* Used by {@link MemberCompletion.Data} implementations to get an AST
* from a source file in a {@link SourceLocation}. Classes should prefer
* this method over calling into the location directly since this method
* caches the most recent result for performance.
*
* @param loc A directory or zip/jar file.
* @param cf The {@link ClassFile} representing the source grab from the
* location.
* @return The compilation unit, or <code>null</code> if it is not found
* or an IO error occurs.
*/
public static CompilationUnit getCompilationUnitFromDisk(
SourceLocation loc, ClassFile cf) {
// Cached value?
if (loc==lastCUFileParam && cf==lastCUClassFileParam) {
//System.out.println("Returning cached CompilationUnit");
return lastCUFromDisk;
}
lastCUFileParam = loc;
lastCUClassFileParam = cf;
CompilationUnit cu = null;
if(loc != null) {
try {
cu = loc.getCompilationUnit(cf);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
lastCUFromDisk = cu;
return cu;
}
/**
* Returns the "unqualified" version of a (possibly) fully-qualified
* class name.
*
* @param clazz The class name.
* @return The unqualified version of the name.
*/
public static final String getUnqualified(String clazz) {
int dot = clazz.lastIndexOf('.');
if (dot>-1) {
clazz = clazz.substring(dot+1);
}
return clazz;
}
/**
* Returns the next location of a single character in a character sequence.
* This method is here because <tt>StringBuilder</tt> doesn't get this
* method added to it until Java 1.5.
*
* @param ch The character to look for.
* @param sb The character sequence.
* @param offs The offset at which to start looking.
* @return The next location of the character, or <tt>-1</tt> if it is not
* found.
*/
private static final int indexOf(char ch, CharSequence sb, int offs) {
while (offs<sb.length()) {
if (ch==sb.charAt(offs)) {
return offs;
}
offs++;
}
return -1;
}
/**
* Returns whether the specified string is "fully qualified," that is,
* whether it contains a '<code>.</code>' character.
*
* @param str The string to check.
* @return Whether the string is fully qualified.
* @see #getUnqualified(String)
*/
public static final boolean isFullyQualified(String str) {
return str.indexOf('.')>-1;
}
/**
* Returns whether this line ends in the middle of a pre-block.
*
* @param line The line's contents.
* @param prevValue Whether this line started in a pre-block.
* @return Whether the line ends in a pre-block.
*/
private static final boolean isInPreBlock(String line, boolean prevValue) {
int lastPre = line.lastIndexOf("pre>");
if (lastPre<=0) {
return prevValue;
}
char prevChar = line.charAt(lastPre-1);
if (prevChar=='<') {
return true;
}
else if (prevChar=='/' && lastPre>=2) {
if (line.charAt(lastPre-2)=='<') {
return false;
}
}
return prevValue;
}
/**
* Removes the tail end of a documentation comment from a string, if it
* exists.
*
* @param str The string.
* @return The string, possibly with the documentation comment tail
* removed.
*/
private static final String possiblyStripDocCommentTail(String str) {
if (str.endsWith("*/")) {
str = str.substring(0, str.length()-2);
}
return str;
}
/**
* A faster way to split on a single char than String#split(), since
* we'll be doing this in a tight loop possibly thousands of times (rt.jar).
* This is also fundamentally different than {@link String#split(String)}),
* in the case where <code>str</code> ends with <code>ch</code> - this
* method will return an empty item at the end of the returned array, while
* String#split() will not.
*
* @param str The string to split.
* @param ch The char to split on.
* @return The string, split on the character (e.g. '<tt>/</tt>' or
* '<tt>.</tt>').
*/
public static final String[] splitOnChar(String str, int ch) {
List list = new ArrayList(3);
int pos = 0;
int old = 0;
while ((pos=str.indexOf(ch, old))>-1) {
list.add(str.substring(old, pos));
old = pos+1;
}
// If str ends in ch, this adds an empty item to the end of the list.
// This is what we want.
list.add(str.substring(old));
String[] array = new String[list.size()];
return (String[])list.toArray(array);
}
}