/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* CALDocToJavaDocUtilities.java
* Creation date: Jul 14, 2006
* By: RCypher
*/
package org.openquark.cal.caldoc;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openquark.cal.compiler.CALDocComment;
import org.openquark.cal.compiler.ClassMethod;
import org.openquark.cal.compiler.DataConstructor;
import org.openquark.cal.compiler.Function;
import org.openquark.cal.compiler.FunctionalAgent;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.compiler.CALDocComment.ArgBlock;
import org.openquark.cal.compiler.CALDocComment.ModuleReference;
import org.openquark.cal.compiler.CALDocComment.ScopedEntityReference;
import org.openquark.cal.compiler.CALDocComment.TextBlock;
/**
* This is a utility class containing helper methods for converting CALDoc into Javadoc.
*
* @author RCypher (original version based on {@link CALDocToTextUtilities})
* @author Joseph Wong (revised HTML-based version)
*/
public class CALDocToJavaDocUtilities {
/**
* An abstract class representing a cross-reference generator that can generate Javadoc references.
*
* @author Joseph Wong
*/
public static abstract class JavadocCrossReferenceGenerator extends CALDocToHTMLUtilities.CrossReferenceHTMLStringGenerator {
/**
* Runs any post-processing (e.g. unescaping) required.
* @param html the HTML to be procsseed.
* @return the processed result.
*/
public String postProcess(final String html) {
return html;
}
}
/**
* Implements a cross-reference generator capable of generating non-hyperlinked text for cross-references.
*
* @author Joseph Wong
*/
private static final class NoHyperlinkCrossReferenceGenerator extends JavadocCrossReferenceGenerator {
/** {@inheritDoc} */
@Override
public String getModuleReferenceHTML(final ModuleReference reference) {
return reference.getName().toSourceText();
}
/** {@inheritDoc} */
@Override
public String getTypeConsReferenceHTML(final ScopedEntityReference reference) {
return reference.getName().getQualifiedName();
}
/** {@inheritDoc} */
@Override
public String getDataConsReferenceHTML(final ScopedEntityReference reference) {
return reference.getName().getQualifiedName();
}
/** {@inheritDoc} */
@Override
public String getFunctionOrClassMethodReferenceHTML(final ScopedEntityReference reference) {
return reference.getName().getQualifiedName();
}
/** {@inheritDoc} */
@Override
public String getTypeClassReferenceHTML(final ScopedEntityReference reference) {
return reference.getName().getQualifiedName();
}
}
/** Private constructor. */
private CALDocToJavaDocUtilities() {}
/**
* Generates the argument names from the CALDoc comment.
* @param caldoc the CALDoc comment of the entity. Can be null.
* @return the argument names from the CALDoc comment.
*/
public static String[] getArgumentNamesFromCALDocComment(final CALDocComment caldoc) {
return getArgumentNamesFromCALDocComment(caldoc, null);
}
/**
* Generates the argument names from the CALDoc comment.
* @param caldoc the CALDoc comment of the entity. Can be null.
* @param envEntity the FunctionalAgent being documented. Can be null if the comment is not for an FunctionalAgent.
* @return the argument names from the CALDoc comment.
*/
public static String[] getArgumentNamesFromCALDocComment(final CALDocComment caldoc, final FunctionalAgent envEntity) {
final TypeExpr typeExpr = envEntity == null ? null : envEntity.getTypeExpr();
return getArgumentNamesFromCALDocComment(caldoc, envEntity, typeExpr);
}
/**
* Generates the argument names from the CALDoc comment.
* @param caldoc the CALDoc comment of the entity. Can be null.
* @param envEntity the FunctionalAgent being documented. Can be null if the comment is not for an FunctionalAgent.
* @param typeExpr the type of the entity. Can be null if envEntity is null.
* @return the argument names from the CALDoc comment.
*/
public static String[] getArgumentNamesFromCALDocComment(final CALDocComment caldoc, final FunctionalAgent envEntity, final TypeExpr typeExpr) {
/// The arguments
//
final int nArgsInCALDoc = (caldoc == null) ? -1 : caldoc.getNArgBlocks();
final int arity = (typeExpr == null) ? -1 : typeExpr.getArity();
final int nArgs = Math.max(nArgsInCALDoc, arity);
if (nArgs > 0) {
final String argNames[] = new String[nArgs];
final Set<String> setOfArgumentNames = new HashSet<String>();
for (int i = 0; i < nArgs; i++) {
argNames[i] = getNthArgumentName(caldoc, envEntity, i, setOfArgumentNames);
}
return argNames;
}
return new String[]{};
}
/**
* Generates a plain text representation for a CALDoc comment.
* @param caldoc the CALDoc comment.
* @param isClassComment whether the Javadoc comment is a class comment.
* @return a plain text representation for the comment.
*/
public static String getJavadocFromCALDocComment(final CALDocComment caldoc, final boolean isClassComment) {
// even though we specify ScopedEntityNamingPolicy.QUALIFIED here, it has no effect on the generated
// text as there is no FunctionalAgent from which a type signature (potentially containing types in other modules)
// can be extracted.
return getJavadocFromCALDocComment(caldoc, isClassComment, null, null);
}
/**
* Generates a plain text representation for a CALDoc comment.
* @param caldoc the CALDoc comment.
* @param isClassComment whether the Javadoc comment is a class comment.
* @param envEntity the FunctionalAgent being documented. Can be null if the comment is not for an FunctionalAgent.
* @param argNames the names of arguments to use. Can be null if the default mechanism for obtaining argument names is to be used.
* @return a plain text representation for the comment.
*/
public static String getJavadocFromCALDocComment(final CALDocComment caldoc, final boolean isClassComment, final FunctionalAgent envEntity, final String[] argNames) {
TypeExpr typeExpr;
if (envEntity == null) {
typeExpr = null;
} else {
typeExpr = envEntity.getTypeExpr();
}
return getJavadocFromCALDocComment(caldoc, isClassComment, envEntity, typeExpr, argNames, ScopedEntityNamingPolicy.FULLY_QUALIFIED);
}
/**
* Generates a plain text representation for a CALDoc comment.
* @param caldoc the CALDoc comment.
* @param isClassComment whether the Javadoc comment is a class comment.
* @param envEntity the FunctionalAgent being documented. Can be null if the comment is not for an FunctionalAgent.
* @param typeExpr the type of the entity. Can be null if envEntity is null.
* @param argNames the names of arguments to use.
* @param scopedEntityNamingPolicy the naming policy to use for generating qualified/unqualified names.
* @return a plain text representation for the comment.
*/
public static String getJavadocFromCALDocComment(final CALDocComment caldoc, final boolean isClassComment, final FunctionalAgent envEntity, final TypeExpr typeExpr, final String[] argNames, final ScopedEntityNamingPolicy scopedEntityNamingPolicy) {
return getJavadocFromCALDocComment(caldoc, isClassComment, envEntity, typeExpr, argNames, scopedEntityNamingPolicy, new NoHyperlinkCrossReferenceGenerator(), false, false, true, false);
}
/**
* Generates a plain text representation for a CALDoc comment.
* @param caldoc the CALDoc comment.
* @param isClassComment whether the Javadoc comment is a class comment.
* @param envEntity the FunctionalAgent being documented. Can be null if the comment is not for an FunctionalAgent.
* @param typeExpr the type of the entity. Can be null if envEntity is null.
* @param argNames the names of arguments to use. Can be null if the default mechanism for obtaining argument names is to be used.
* @param scopedEntityNamingPolicy the naming policy to use for generating qualified/unqualified names.
* @param crossRefGen the cross reference generator to use.
* @param omitReturnBlock whether to omit the return block.
* @param allowReturnBlockForDataCons whether to allow return block for data cons.
* @param disallowJavadocCodeBlock whether to disallow the Java 5 code block, and always use the HTML version.
* @param trimBlankFirstLine whether to trim the first line if blank.
* @return a plain text representation for the comment.
*/
public static String getJavadocFromCALDocComment(final CALDocComment caldoc, final boolean isClassComment, final FunctionalAgent envEntity, final TypeExpr typeExpr, final String[] argNames, final ScopedEntityNamingPolicy scopedEntityNamingPolicy, final JavadocCrossReferenceGenerator crossRefGen, final boolean omitReturnBlock, final boolean allowReturnBlockForDataCons, final boolean disallowJavadocCodeBlock, final boolean trimBlankFirstLine) {
final StringBuilder buffer = new StringBuilder();
if (caldoc != null) {
/// The main description
//
buffer.append(getJavadocFriendlyHTMLForCALDocTextBlock(caldoc.getDescriptionBlock(), crossRefGen));
/// The "See Also" references - not generated as Javadoc @see blocks
//
final StringBuilder seeBuffer = new StringBuilder();
// module cross references
final int nModuleRefs = caldoc.getNModuleReferences();
if (nModuleRefs > 0) {
seeBuffer.append("\n").append("<dd><b>").append(CALDocMessages.getString("modulesColon")).append("</b> ");
for (int i = 0; i < nModuleRefs; i++) {
if (i > 0) {
seeBuffer.append(CALDocMessages.getString("commaAndSpace"));
}
seeBuffer.append(crossRefGen.postProcess(crossRefGen.getModuleReferenceHTML(caldoc.getNthModuleReference(i))));
}
}
// function and class method references
final int nFuncRefs = caldoc.getNFunctionOrClassMethodReferences();
if (nFuncRefs > 0) {
seeBuffer.append("\n").append("<dd><b>").append(CALDocMessages.getString("functionsAndClassMethodsColon")).append("</b> ");
for (int i = 0; i < nFuncRefs; i++) {
if (i > 0) {
seeBuffer.append(CALDocMessages.getString("commaAndSpace"));
}
seeBuffer.append(crossRefGen.postProcess(crossRefGen.getFunctionOrClassMethodReferenceHTML(caldoc.getNthFunctionOrClassMethodReference(i))));
}
}
// type constructor references
final int nTypeConsRefs = caldoc.getNTypeConstructorReferences();
if (nTypeConsRefs > 0) {
seeBuffer.append("\n").append("<dd><b>").append(CALDocMessages.getString("typeConstructorsColon")).append("</b> ");
for (int i = 0; i < nTypeConsRefs; i++) {
if (i > 0) {
seeBuffer.append(CALDocMessages.getString("commaAndSpace"));
}
seeBuffer.append(crossRefGen.postProcess(crossRefGen.getTypeConsReferenceHTML(caldoc.getNthTypeConstructorReference(i))));
}
}
// data constructor references
final int nDataConsRefs = caldoc.getNDataConstructorReferences();
if (nDataConsRefs > 0) {
seeBuffer.append("\n").append("<dd><b>").append(CALDocMessages.getString("dataConstructorsColon")).append("</b> ");
for (int i = 0; i < nDataConsRefs; i++) {
if (i > 0) {
seeBuffer.append(CALDocMessages.getString("commaAndSpace"));
}
seeBuffer.append(crossRefGen.postProcess(crossRefGen.getDataConsReferenceHTML(caldoc.getNthDataConstructorReference(i))));
}
}
// type class references
final int nTypeClassRefs = caldoc.getNTypeClassReferences();
if (nTypeClassRefs > 0) {
seeBuffer.append("\n").append("<dd><b>").append(CALDocMessages.getString("typeClassesColon")).append("</b> ");
for (int i = 0; i < nTypeClassRefs; i++) {
if (i > 0) {
seeBuffer.append(CALDocMessages.getString("commaAndSpace"));
}
seeBuffer.append(crossRefGen.postProcess(crossRefGen.getTypeClassReferenceHTML(caldoc.getNthTypeClassReference(i))));
}
}
if (seeBuffer.length() > 0) {
buffer.append("\n\n<dl><dt><b>").append(CALDocMessages.getString("seeAlsoColon")).append("</b>").append(seeBuffer.toString()).append("\n</dl>\n");
}
}
/// The arguments and the return value
//
buffer.append(getJavadocFragmentForArgumentsAndReturnValue(caldoc, envEntity, typeExpr, argNames, scopedEntityNamingPolicy, crossRefGen, omitReturnBlock, allowReturnBlockForDataCons, disallowJavadocCodeBlock));
if (caldoc != null) {
// The @author and @version tags are only allowed on overview, package and class documentation, per the Javadoc spec
if (isClassComment) {
/// The authors
//
final int nAuthors = caldoc.getNAuthorBlocks();
if (nAuthors > 0) {
for (int i = 0; i < nAuthors; i++) {
buffer.append("\n").append("@author ");
final TextBlock authorBlock = caldoc.getNthAuthorBlock(i);
buffer.append(getJavadocFriendlyHTMLForCALDocTextBlock(authorBlock, crossRefGen));
}
}
/// The version
//
final TextBlock versionBlock = caldoc.getVersionBlock();
if (versionBlock != null) {
buffer.append("\n").append("@version ");
buffer.append(getJavadocFriendlyHTMLForCALDocTextBlock(versionBlock, crossRefGen));
}
}
/// The deprecated block comes after all other blocks, according to the Javadoc style guide.
//
final TextBlock deprecatedBlock = caldoc.getDeprecatedBlock();
if (deprecatedBlock != null) {
buffer.append("\n").append("@deprecated ");
buffer.append(getJavadocFriendlyHTMLForCALDocTextBlock(deprecatedBlock, crossRefGen));
}
}
// Wrap the comment text with comment delimiters
if (buffer.length() > 0 && buffer.charAt(0) == '\n') {
if (trimBlankFirstLine) {
// If the first char is a newline, trim it so that we don't get a blank line as the first line
buffer.deleteCharAt(0);
}
}
final String newline = System.getProperty("line.separator", "\r\n");
String comment =
"/**" + newline +
" * " + buffer.toString().replaceAll("[\r\n]", newline + " * ").replaceAll("[\r\n] \\* $", "") + newline +
" */" + newline;
// Check for invalid unicode escapes that people might
// have inadvertently put into CALDoc comments.
final Pattern p = Pattern.compile("\\\\u[^a-fA-F0-9]");
Matcher m = p.matcher(comment);
while (m.find()) {
final int start = m.start();
comment = comment.substring(0, start + 1) + " " + comment.substring(start+1);
m = p.matcher(comment);
}
return comment;
}
/**
* Generates the HTML for a text block appearing in a CALDoc comment, with appropriate escaping for Javadoc.
* @param textBlock the text block to be formatted as HTML.
* @param crossRefGen the cross reference generator to use.
* @return the HTML for the text block.
*/
private static String getJavadocFriendlyHTMLForCALDocTextBlock(final CALDocComment.TextBlock textBlock, final JavadocCrossReferenceGenerator crossRefGen) {
String html = CALDocToHTMLUtilities.getHTMLForCALDocTextBlock(textBlock, crossRefGen);
// if we start a new paragraph with <p>, we should have a newline right after the tag
html = html.replaceAll("<p>", "<p>\n");
// need to escape out the '@' character, which is always invalid in Javadoc
html = html.replaceAll("@", "@");
// need also to escape out the string "*/"
html = html.replaceAll("\\*/", "*/");
// finally unescape anything escaped by the cross reference generator
html = crossRefGen.postProcess(html);
return html;
}
/**
* Generates a plain text representation for the arguments and return value of a function, class method, or data constructor.
* @param caldoc the associated CALDoc. Can be null.
* @param envEntity the associated entity. Can be null.
* @param typeExpr the type of the entity. Can be null if envEntity is null.
* @param argNames the names of arguments to use. Can be null if the default mechanism for obtaining argument names is to be used.
* @param scopedEntityNamingPolicy the naming policy to use for generating qualified/unqualified names.
* @param crossRefGen the cross reference generator to use.
* @param omitReturnBlock whether to omit the return block.
* @param allowReturnBlockForDataCons whether to allow return block for data cons.
* @param disallowJavadocCodeBlock whether to disallow the Java 5 code block, and always use the HTML version.
* @return the JavaDoc text for the arguments and return value.
*/
private static String getJavadocFragmentForArgumentsAndReturnValue(final CALDocComment caldoc, final FunctionalAgent envEntity, final TypeExpr typeExpr, String[] argNames, final ScopedEntityNamingPolicy scopedEntityNamingPolicy, final JavadocCrossReferenceGenerator crossRefGen, boolean omitReturnBlock, final boolean allowReturnBlockForDataCons, final boolean disallowJavadocCodeBlock) {
final StringBuilder buffer = new StringBuilder();
/// The arguments
//
if (argNames == null) {
argNames = getArgumentNamesFromCALDocComment(caldoc, envEntity, typeExpr);
}
TypeExpr[] typePieces = null;
String[] typePieceStrings = null;
if (typeExpr != null) {
typePieces = typeExpr.getTypePieces();
typePieceStrings = TypeExpr.toStringArray(typePieces, true, scopedEntityNamingPolicy);
}
if (argNames.length > 0) {
for (int i = 0; i < argNames.length; i++) {
buffer.append("\n@param ").append(argNames[i]).append(" ");
if (typePieces != null) {
final String argTypeHTML = getTypeAsFormattedString(typePieceStrings[i], disallowJavadocCodeBlock);
buffer.append(CALDocMessages.getString("calTypeIndicator", argTypeHTML));
}
if (caldoc != null && i < caldoc.getNArgBlocks()) {
final ArgBlock argBlock = caldoc.getNthArgBlock(i);
final String argDescription = getJavadocFriendlyHTMLForCALDocTextBlock(argBlock.getTextBlock(), crossRefGen);
if (typePieces != null && argDescription.trim().length() > 0) {
// if there is both a type description and an arg description, separate them
// with a newline and some indentation
buffer.append("\n ");
}
buffer.append(argDescription);
}
}
}
if (!omitReturnBlock) {
/// The return value
//
final boolean hasReturnBlock = (caldoc != null) && (caldoc.getReturnBlock() != null);
// Note that we generate a return block not only for functions and class methods, but also for data constructors
// as well (even though they *cannot* be documented with a @return block in CALDoc) because we are generating
// *Javadoc*, and the CAL type of the data constructor is extremely important and should be documented!
if (hasReturnBlock || envEntity instanceof Function || envEntity instanceof ClassMethod || (envEntity instanceof DataConstructor && allowReturnBlockForDataCons)) {
buffer.append("\n").append("@return ");
if (typePieces != null) {
final String returnTypeHTML = getTypeAsFormattedString(typePieceStrings[argNames.length], disallowJavadocCodeBlock);
buffer.append(CALDocMessages.getString("calTypeIndicator", returnTypeHTML)).append(" ");
}
if (hasReturnBlock) {
final TextBlock returnBlock = caldoc.getReturnBlock();
final String returnDescription = getJavadocFriendlyHTMLForCALDocTextBlock(returnBlock, crossRefGen);
if (typePieces != null && returnDescription.trim().length() > 0) {
// if there is both a type description and an return description, separate them
// with a newline and some indentation
buffer.append("\n ");
}
buffer.append(returnDescription);
}
}
}
return buffer.toString();
}
/**
* Converts a CAL type into a formatted Javadoc string.
* @param typeString the type as a string.
* @param disallowJavadocCodeBlock whether to disallow the Java 5 code block, and always use the HTML version.
* @return the formatted version.
*/
private static String getTypeAsFormattedString(final String typeString, final boolean disallowJavadocCodeBlock) {
// We do not use CALDocToHTMLUtilities.htmlEscape() to escape the type string
// since we know it will not contain tag-like sequences <...>, and the result
// of turning => and -> to => and -> is ugly.
// To make the type look nice, if the type does not contain { or }, we can use the Java 5 {@code},
// otherwise we wrap the type with a <code> block.
if (typeString.contains("{") || typeString.contains("}") || disallowJavadocCodeBlock) {
return "<code>" + typeString + "</code>";
} else {
return "{@code " + typeString + "}";
}
}
/**
* Generates an argument name of an FunctionalAgent. It gives preference to the name given in
* CALDoc, if it is different from the name appearing in the entity itself.
*
* @param caldoc the CALDoc comment of the entity. Can be null.
* @param envEntity the FunctionalAgent whose argument name is being generated.
* @param index the position of the argument in the argument list.
* @param setOfArgumentNames the (Set of Strings) of argument names already used (for disambiguation purposes).
* @return the appropriate argument name.
*/
private static String getNthArgumentName(final CALDocComment caldoc, final FunctionalAgent envEntity, final int index, final Set<String> setOfArgumentNames) {
////
/// First fetch the name from the entity. This will mostly be the same name as the one appearing in code, except for
/// foreign functions, which may have their sames extracted from the Java classes' debug info.
//
String nameFromEntity = null;
if (index < envEntity.getNArgumentNames()) {
nameFromEntity = envEntity.getArgumentName(index);
}
String result;
if (caldoc != null && index < caldoc.getNArgBlocks()) {
////
/// Get the CALDoc name, if available.
//
final String nameFromCALDoc = caldoc.getNthArgBlock(index).getArgName().getCalSourceForm();
result = nameFromCALDoc;
setOfArgumentNames.add(nameFromCALDoc);
} else if (nameFromEntity != null) {
////
/// If the entity yielded a name, use it.
//
result = nameFromEntity;
setOfArgumentNames.add(nameFromEntity);
} else {
////
/// Since not even the entity yielded a name, construct an artificial one of the form arg_x, where x is the
/// 1-based index of the argument in the argument list, or of the form arg_x_y if arg_x, arg_x_1, ...
/// arg_x_(y-1) have all appeared previously in the argument list.
//
// the base artificial name we'll attempt to use is arg_x, where x is the 1-based index of this argument
final String baseArtificialName = "arg_" + (index + 1);
String artificialName = baseArtificialName;
// if the base artificial name already appears in previous arguments, then
// make the argument name arg_x_y, where y is a supplementary disambiguating number
// chosen so that the resulting name will not collide with any of the previous argument names
int supplementaryDisambiguator = 1;
while (setOfArgumentNames.contains(artificialName)) {
artificialName = baseArtificialName + "_" + supplementaryDisambiguator;
supplementaryDisambiguator++;
}
result = artificialName;
setOfArgumentNames.add(artificialName);
}
return result;
}
}