/* *******************************************************************
* Copyright (c) 1999-2001 Xerox Corporation,
* 2002 Palo Alto Research Center, Incorporated (PARC).
* 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:
* Xerox/PARC initial implementation
* Mik Kersten port to AspectJ 1.1+ code base
* ******************************************************************/
package org.aspectj.tools.ajdoc;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.aspectj.asm.AsmManager;
import org.aspectj.asm.HierarchyWalker;
import org.aspectj.asm.IProgramElement;
import org.aspectj.asm.IRelationship;
import org.aspectj.util.TypeSafeEnum;
/**
* @author Mik Kersten
*/
class HtmlDecorator {
private static final String POINTCUT_DETAIL = "Pointcut Detail";
private static final String ADVICE_DETAIL = "Advice Detail";
private static final String DECLARE_DETAIL = "Declare Detail";
private static final String ADVICE_SUMMARY = "Advice Summary";
private static final String POINTCUT_SUMMARY = "Pointcut Summary";
private static final String DECLARE_SUMMARY = "Declare Summary";
private static final String ITD_METHOD_SUMMARY = "Inter-Type Method Summary";
private static final String ITD_FIELD_SUMMARY = "Inter-Type Field Summary";
private static final String ITD_CONSTRUCTOR_SUMMARY = "Inter-Type Constructor Summary";
static List visibleFileList = new ArrayList();
static Hashtable declIDTable = null;
static File rootDir = null;
static String docVisibilityModifier;
static void decorateHTMLFromInputFiles(AsmManager model, Hashtable table, File newRootDir, File[] inputFiles, String docModifier)
throws IOException {
rootDir = newRootDir;
declIDTable = table;
docVisibilityModifier = docModifier;
for (int i = 0; i < inputFiles.length; i++) {
decorateHTMLFromIPEs(getProgramElements(model, inputFiles[i].getCanonicalPath()), rootDir.getCanonicalPath()
+ Config.DIR_SEP_CHAR, docModifier, false);
}
}
static void decorateHTMLFromIPEs(IProgramElement[] decls, String base, String docModifier, boolean exceededNestingLevel)
throws IOException {
if (decls != null) {
for (int i = 0; i < decls.length; i++) {
IProgramElement decl = decls[i];
decorateHTMLFromIPE(decl, base, docModifier, exceededNestingLevel);
}
}
}
/**
* Before attempting to decorate the HTML file we have to verify that it exists, which depends on the documentation visibility
* specified to c.
*
* Depending on docModifier, can document - public: only public - protected: protected and public (default) - package: package
* protected and public - private: everything
*/
static void decorateHTMLFromIPE(IProgramElement decl, String base, String docModifier, boolean exceededNestingLevel)
throws IOException {
boolean nestedClass = false;
if (decl.getKind().isType()) {
boolean decorateFile = true;
if (isAboveVisibility(decl)) {
visibleFileList.add(decl.toSignatureString());
String packageName = decl.getPackageName();
String filename = "";
if (packageName != null) {
int index1 = base.lastIndexOf(Config.DIR_SEP_CHAR);
int index2 = base.lastIndexOf(".");
String currFileClass = "";
if (index1 > -1 && index2 > 0 && index1 < index2) {
currFileClass = base.substring(index1 + 1, index2);
}
// XXX only one level of nexting
if (currFileClass.equals(decl.getDeclaringType())) {
nestedClass = true;
packageName = packageName.replace('.', '/');
String newBase = "";
if (base.lastIndexOf(Config.DIR_SEP_CHAR) > 0) {
newBase = base.substring(0, base.lastIndexOf(Config.DIR_SEP_CHAR));
}
String signature = constructNestedTypeName(decl);
filename = newBase + Config.DIR_SEP_CHAR + packageName + Config.DIR_SEP_CHAR + currFileClass + // "." +
signature + ".html";
} else {
packageName = packageName.replace('.', '/');
filename = base + packageName + Config.DIR_SEP_CHAR + decl.toSignatureString() + ".html";
}
} else {
filename = base + decl.toSignatureString() + ".html";
}
if (!exceededNestingLevel) {
decorateHTMLFile(new File(filename));
} else {
System.out.println("Warning: can not generate documentation for nested " + "inner class: "
+ decl.toSignatureString());
}
}
}
}
private static String constructNestedTypeName(IProgramElement node) {
if (node.getParent().getKind().isSourceFile()) {
return node.getName();
} else {
String nodeName = "";
if (node.getKind().isType())
nodeName += '.' + node.getName();
return constructNestedTypeName(node.getParent()) + nodeName;
}
}
/**
* Skips files that are public in the model but not public in the source, e.g. nested aspects.
*/
static void decorateHTMLFile(File file) throws IOException {
if (!file.exists())
return;
System.out.println("> Decorating " + file.getCanonicalPath() + "...");
BufferedReader reader = new BufferedReader(new FileReader(file));
StringBuffer fileContents = new StringBuffer();
String line = reader.readLine();
while (line != null) {
fileContents.append(line + "\n");
line = reader.readLine();
}
boolean isSecond = false;
int index = 0;
IProgramElement decl;
while (true) {
// ---this next part is an inlined procedure that returns two values---
// ---the next declaration and the index at which that declaration's---
// ---DeclID sits in the .html file ---
String contents = fileContents.toString();
int start = contents.indexOf(Config.DECL_ID_STRING, index);
int end = contents.indexOf(Config.DECL_ID_TERMINATOR, index);
if (start == -1)
decl = null;
else if (end == -1)
throw new Error("Malformed DeclID.");
else {
String tid = contents.substring(start + Config.DECL_ID_STRING.length(), end);
decl = (IProgramElement) declIDTable.get(tid);
index = start;
}
// --- ---
// --- ---
if (decl == null)
break;
fileContents.delete(start, end + Config.DECL_ID_TERMINATOR.length());
if (decl.getKind().isType()) {
isSecond = true;
String fullname = "";
if (decl.getParent().getKind().equals(IProgramElement.Kind.ASPECT)
|| decl.getParent().getKind().equals(IProgramElement.Kind.CLASS)) {
fullname += decl.getParent().toSignatureString().concat(".").concat(decl.toSignatureString());
} else {
fullname += decl.toSignatureString();
}
// only add aspect documentation if we're in the correct
// file for the given IProgramElement
if (file.getName().indexOf(fullname + ".html") != -1) {
addAspectDocumentation(decl, fileContents, index);
}
} else {
decorateMemberDocumentation(decl, fileContents, index);
}
// Change "Class" to "Aspect"
// moved this here because then can use the IProgramElement.Kind
// rather than checking to see if there's advice - this fixes
// the case with an inner aspect not having the title "Aspect"
if (decl.getKind().equals(IProgramElement.Kind.ASPECT) && file.getName().indexOf(decl.toSignatureString()) != -1) {
// only want to change "Class" to "Aspect" if we're in the
// file corresponding to the IProgramElement
String fullname = "";
if (decl.getParent().getKind().equals(IProgramElement.Kind.ASPECT)
|| decl.getParent().getKind().equals(IProgramElement.Kind.CLASS)) {
fullname += decl.getParent().toSignatureString().concat(".").concat(decl.toSignatureString());
} else {
fullname += decl.toSignatureString();
}
if (file.getName().indexOf(fullname + ".html") == -1) {
// we're still in the file for a parent IPE
continue;
}
boolean br = true;
int classStartIndex = fileContents.toString().indexOf("
\nClass ");
if (classStartIndex == -1) {
classStartIndex = fileContents.toString().indexOf("
static class ClassA.InnerAspect classStartIndex = fileContents.toString().indexOf("class "); int classEndIndex = fileContents.toString().indexOf("", classStartIndex); if (classEndIndex != -1) { // Convert it to "aspect ClassA.InnerAspect" String classLine = fileContents.toString().substring(classStartIndex, classEndIndex); String aspectLine = "aspect"+fileContents.substring(classStartIndex+5,classEndIndex); fileContents.delete(classStartIndex, classEndIndex); fileContents.insert(classStartIndex, aspectLine); } } } } file.delete(); FileOutputStream fos = new FileOutputStream(file); fos.write(fileContents.toString().getBytes()); reader.close(); fos.close(); } static void addAspectDocumentation(IProgramElement node, StringBuffer fileBuffer, int index) { List pointcuts = new ArrayList(); List advice = new ArrayList(); List declares = new ArrayList(); List methodsDeclaredOn = StructureUtil.getDeclareInterTypeTargets(node, IProgramElement.Kind.INTER_TYPE_METHOD); if (methodsDeclaredOn != null && !methodsDeclaredOn.isEmpty()) { insertDeclarationsSummary(fileBuffer, methodsDeclaredOn, ITD_METHOD_SUMMARY, index); } List fieldsDeclaredOn = StructureUtil.getDeclareInterTypeTargets(node, IProgramElement.Kind.INTER_TYPE_FIELD); if (fieldsDeclaredOn != null && !fieldsDeclaredOn.isEmpty()) { insertDeclarationsSummary(fileBuffer, fieldsDeclaredOn, ITD_FIELD_SUMMARY, index); } List constDeclaredOn = StructureUtil.getDeclareInterTypeTargets(node, IProgramElement.Kind.INTER_TYPE_CONSTRUCTOR); if (fieldsDeclaredOn != null && !constDeclaredOn.isEmpty()) { insertDeclarationsSummary(fileBuffer, constDeclaredOn, ITD_CONSTRUCTOR_SUMMARY, index); } for (Iterator it = node.getChildren().iterator(); it.hasNext();) { IProgramElement member = (IProgramElement) it.next(); if (member.getKind().equals(IProgramElement.Kind.POINTCUT)) { pointcuts.add(member); } else if (member.getKind().equals(IProgramElement.Kind.ADVICE)) { advice.add(member); } else if (member.getKind().isDeclare() || member.getKind().isInterTypeMember()) { declares.add(member); } } if (declares.size() > 0) { insertDeclarationsDetails(fileBuffer, declares, DECLARE_DETAIL, index); insertDeclarationsSummary(fileBuffer, declares, DECLARE_SUMMARY, index); } if (pointcuts.size() > 0) { insertDeclarationsSummary(fileBuffer, pointcuts, POINTCUT_SUMMARY, index); insertDeclarationsDetails(fileBuffer, pointcuts, POINTCUT_DETAIL, index); } if (advice.size() > 0) { insertDeclarationsSummary(fileBuffer, advice, ADVICE_SUMMARY, index); insertDeclarationsDetails(fileBuffer, advice, ADVICE_DETAIL, index); } // add the 'aspect declarations' information against the type List parentsDeclaredOn = StructureUtil.getDeclareInterTypeTargets(node, IProgramElement.Kind.DECLARE_PARENTS); if (parentsDeclaredOn != null && parentsDeclaredOn.size() > 0) { decorateDocWithRel(node, fileBuffer, index, parentsDeclaredOn, HtmlRelationshipKind.ASPECT_DECLARATIONS); } // add the 'annotated by' information against the type List annotatedBy = StructureUtil.getTargets(node, IRelationship.Kind.DECLARE_INTER_TYPE, "annotated by"); if (annotatedBy != null && annotatedBy.size() > 0) { decorateDocWithRel(node, fileBuffer, index, annotatedBy, HtmlRelationshipKind.ANNOTATED_BY); } // add the 'advised by' information against the type List advisedBy = StructureUtil.getTargets(node, IRelationship.Kind.ADVICE); if (advisedBy != null && advisedBy.size() > 0) { decorateDocWithRel(node, fileBuffer, index, advisedBy, HtmlRelationshipKind.ADVISED_BY); } } static void insertDeclarationsSummary(StringBuffer fileBuffer, List decls, String kind, int index) { if (!declsAboveVisibilityExist(decls)) return; int insertIndex = findSummaryIndex(fileBuffer, index); // insert the head of the table String tableHead = "\n\n" + "
" + "" + kind + " | ||
" + "" + "" + generateSignatures(decl)
+ " "; if (!comment.equals("")) { entry += comment + " "; } entry += generateAffects(decl) + " | " + "\n"; } else if (kind.equals(POINTCUT_SUMMARY)) { entry += " | |
" + "" + genAccessibility(decl) + "" + " | \n" + "" + "" + decl.toLabelString()
+ " "; if (!comment.equals("")) { entry += comment + " "; } entry += " | |
" + "" + generateModifierInformation(decl, false) + "" + " | " + "" + "" + ""
+ decl.toLabelString() + " " + generateAffects(decl); } else if (kind.equals(ITD_FIELD_SUMMARY) || kind.equals(ITD_METHOD_SUMMARY)) { entry += " | |
" + "" + generateModifierInformation(decl, false) + "" + " | " + "" + "" + ""
+ decl.toLabelString() + " " + generateDeclaredBy(decl); } else if (kind.equals(ITD_CONSTRUCTOR_SUMMARY)) { entry += " | " + "" + "" + decl.toLabelString()
+ " " + generateDeclaredBy(decl); } // insert the entry fileBuffer.insert(insertIndex, entry); insertIndex += entry.length(); } } // insert the end of the table String tableTail = " |
\n"; fileBuffer.insert(insertIndex, tableTail); insertIndex += tableTail.length(); } private static boolean declsAboveVisibilityExist(List decls) { boolean exist = false; for (Iterator it = decls.iterator(); it.hasNext();) { IProgramElement element = (IProgramElement) it.next(); if (isAboveVisibility(element)) exist = true; } return exist; } private static boolean isAboveVisibility(IProgramElement element) { IProgramElement.Accessibility acc = element.getAccessibility(); if (docVisibilityModifier.equals("private")) { // show all classes and members return true; } else if (docVisibilityModifier.equals("package")) { // show package, protected and public classes and members return acc.equals(IProgramElement.Accessibility.PACKAGE) || acc.equals(IProgramElement.Accessibility.PROTECTED) || acc.equals(IProgramElement.Accessibility.PUBLIC); } else if (docVisibilityModifier.equals("protected")) { // show protected and public classes and members return acc.equals(IProgramElement.Accessibility.PROTECTED) || acc.equals(IProgramElement.Accessibility.PUBLIC); } else if (docVisibilityModifier.equals("public")) { // show public classes and members return acc.equals(IProgramElement.Accessibility.PUBLIC); } return false; } private static String genAccessibility(IProgramElement decl) { if (decl.getAccessibility().equals(IProgramElement.Accessibility.PACKAGE)) { return "(package private)"; } else { return decl.getAccessibility().toString(); } } static void insertDeclarationsDetails(StringBuffer fileBuffer, List decls, String kind, int index) { if (!declsAboveVisibilityExist(decls)) return; int insertIndex = findDetailsIndex(fileBuffer, index); // insert the table heading String detailsHeading = "
\n" + "\n\n" + "
\n" + "" + kind + " | \n" + "
"; entry += "" + generateSignatures(decl) + "\n" + "
" + generateDetailsComment(decl) + "
" + generateAffects(decl); } else if (kind.equals(POINTCUT_DETAIL)) { entry += "
" + generateDetailsComment(decl); } else if (kind.equals(DECLARE_DETAIL)) { entry += "
" + generateModifierInformation(decl, true); if (!decl.getKind().equals(IProgramElement.Kind.INTER_TYPE_CONSTRUCTOR)) { entry += " "; } // if we're not a declare statement then we need to generate the signature. // If we did this for declare statements we get two repeated lines if (!decl.getKind().isDeclare()) { String sigs = generateSignatures(decl); entry += sigs + "
"; } entry += generateAffects(decl) + generateDetailsComment(decl); } // insert the entry if (i != decls.size() - 1) { entry += "
"; } fileBuffer.insert(insertIndex, entry); insertIndex += entry.length(); } } } /** * TODO: don't place the summary first. */ static int findSummaryIndex(StringBuffer fileBuffer, int index) { String fbs = fileBuffer.toString(); String MARKER_1 = ""; String MARKER_2 = ""; int index1 = fbs.indexOf(MARKER_1, index); int index2 = fbs.indexOf(MARKER_2, index); if (index1 < index2 && index1 != -1) { return index1; } else if (index2 != -1) { return index2; } else { return index; } } static int findDetailsIndex(StringBuffer fileBuffer, int index) { String fbs = fileBuffer.toString(); String MARKER_1 = ""; String MARKER_2 = ""; String MARKER_3 = ""; int index1 = fbs.indexOf(MARKER_1, index); int index2 = fbs.indexOf(MARKER_2, index); int index3 = fbs.indexOf(MARKER_3, index); if (index1 != -1 && index1 < index2 && index1 < index3) { return index1; } else if (index2 != -1 && index2 < index1 && index2 < index3) { return index2; } else if (index3 != -1) { return index3; } else { return index; } } static void decorateDocWithRel(IProgramElement node, StringBuffer fileContentsBuffer, int index, List targets, HtmlRelationshipKind relKind) { if (targets != null && !targets.isEmpty()) { String adviceDoc = "
" + relKind.toString() + " | "; String relativePackagePath = getRelativePathFromHere(node.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR); List addedNames = new ArrayList(); for (Iterator it = targets.iterator(); it.hasNext();) { Object o = it.next(); IProgramElement currDecl = null; if (o instanceof String) { String currHandle = (String) o; currDecl = node.getModel().getHierarchy().findElementForHandle(currHandle); } else if (o instanceof IProgramElement) { currDecl = (IProgramElement) o; } else { return; } String packagePath = ""; if (currDecl.getPackageName() != null && !currDecl.getPackageName().equals("")) { packagePath = currDecl.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR; } String hrefName = ""; String hrefLink = ""; // Start the hRefLink with the relative path based on where // *this* type (i.e. the advised) is in the package structure. hrefLink = relativePackagePath + packagePath; if (currDecl.getPackageName() != null) { hrefName = currDecl.getPackageName().replace('.', '/'); } // in the case of nested classes, in order for the links to work, // need to have the correct file name which is something of the // form parentClass.nestedAspect.html List names = new ArrayList(); IProgramElement parent = currDecl; while (parent != null && parent.getParent() != null && (!parent.getParent().getKind().equals(IProgramElement.Kind.FILE_JAVA) && !parent.getParent().getKind() .equals(IProgramElement.Kind.FILE_ASPECTJ))) { parent = parent.getParent(); names.add(parent.toLinkLabelString()); } StringBuffer sbuff = new StringBuffer(); for (int i = names.size() - 1; i >= 0; i--) { String element = (String) names.get(i); if (i == 0) { sbuff.append(element); } else { sbuff.append(element + "."); } } // use the currDecl.toLabelString rather than currDecl.getName() // because two distinct advice blocks can have the same // currDecl.getName() and wouldn't both appear in the ajdoc hrefName += Config.DIR_SEP_CHAR + sbuff.toString() + "." + currDecl.toLabelString(); // need to replace " with quot; otherwise the links wont work // for 'matches declare' relationship StringBuffer sb = new StringBuffer(currDecl.toLabelString()); int nextQuote = sb.toString().indexOf("\""); while (nextQuote != -1) { sb.deleteCharAt(nextQuote); sb.insert(nextQuote, "quot;"); nextQuote = sb.toString().indexOf("\""); } hrefLink += sbuff.toString() + ".html" + "#" + sb.toString(); if (!addedNames.contains(hrefName)) { adviceDoc = adviceDoc + "" + hrefName.replace('/', '.') + ""; if (it.hasNext()) adviceDoc += ", "; addedNames.add(hrefName); } } adviceDoc += " |
" + " Declared by: | "; String relativePackagePath = getRelativePathFromHere(decl.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR); if (decl != null && !StructureUtil.isAnonymous(decl.getParent())) { String packagePath = ""; if (decl.getPackageName() != null && !decl.getPackageName().equals("")) { packagePath = decl.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR; } String typeSignature = constructNestedTypeName(decl); String hrefName = packagePath + typeSignature; // The hrefLink needs to just be the corresponding aspect String hrefLink = relativePackagePath + packagePath + typeSignature + ".html"; entry += "" + hrefName.replace('/', '.') + ""; // !!! don't replace } entry += " |
" + HtmlRelationshipKind.ADVISES.toString() + " | "; } else if (kind.equals(IProgramElement.Kind.DECLARE_WARNING) || kind.equals(IProgramElement.Kind.DECLARE_ERROR)) { entry += " | " + HtmlRelationshipKind.MATCHED_BY.toString() + " | "; } else if (kind.isDeclareAnnotation()) { entry += " | " + HtmlRelationshipKind.ANNOTATES.toString() + " | "; } else if (kind.equals(IProgramElement.Kind.DECLARE_SOFT)) { entry += " | " + HtmlRelationshipKind.SOFTENS.toString() + " | "; } else { entry += " | " + HtmlRelationshipKind.DECLARED_ON.toString() + " | "; } String relativePackagePath = getRelativePathFromHere(decl.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR); List addedNames = new ArrayList(); // for ensuring that we don't add duplciates for (Iterator it = targets.iterator(); it.hasNext();) { String currHandle = (String) it.next(); IProgramElement currDecl = decl.getModel().getHierarchy().findElementForHandle(currHandle); if (currDecl.getKind().equals(IProgramElement.Kind.CODE)) { currDecl = currDecl.getParent(); // promote to enclosing } if (currDecl != null && !StructureUtil.isAnonymous(currDecl.getParent())) { String packagePath = ""; if (currDecl.getPackageName() != null && !currDecl.getPackageName().equals("")) { packagePath = currDecl.getPackageName().replace('.', '/') + Config.DIR_SEP_CHAR; } String typeSignature = constructNestedTypeName(currDecl); String hrefName = packagePath + typeSignature; // Start the hRefLink with the relative path based on where // *this* type (i.e. the advisor) is in the package structure. String hrefLink = relativePackagePath + packagePath + typeSignature + ".html"; if (!currDecl.getKind().isType()) { hrefName += '.' + currDecl.getName(); hrefLink += "#" + currDecl.toLabelString(); } if (!addedNames.contains(hrefName)) { entry += "" + hrefName.replace('/', '.') + ""; // !!! don't replace if (it.hasNext()) entry += ", "; addedNames.add(hrefName); } } } entry += " |
packagePath
.
*/
private static String getRelativePathFromHere(String packagePath) {
StringBuffer result = new StringBuffer("");
if (packagePath != null && (packagePath.indexOf("/") != -1)) {
StringTokenizer sTok = new StringTokenizer(packagePath, "/", false);
while (sTok.hasMoreTokens()) {
sTok.nextToken(); // don't care about the token value
result.append(".." + Config.DIR_SEP_CHAR);
}// end while
}// end if
return result.toString();
}
/**
* Generate the "public int"-type information about the given IProgramElement. Used when dealing with ITDs. To mirror the
* behaviour of methods and fields in classes, if we're generating the summary information we don't want to include "public" if
* the accessibility of the IProgramElement is public.
*
*/
private static String generateModifierInformation(IProgramElement decl, boolean isDetails) {
String intro = "";
if (decl.getKind().isDeclare()) {
return intro + "";
}
if (isDetails || !decl.getAccessibility().equals(IProgramElement.Accessibility.PUBLIC)) {
intro += "" + decl.getAccessibility().toString() + " ";
}
if (decl.getKind().equals(IProgramElement.Kind.INTER_TYPE_FIELD)) {
return intro + decl.getCorrespondingType() + "";
} else if (decl.getKind().equals(IProgramElement.Kind.INTER_TYPE_CONSTRUCTOR) && isDetails) {
return intro + "";
} else {
return intro + decl.getCorrespondingType(true) + "";
}
}
static String generateIntroductionSignatures(IProgramElement decl, boolean isDetails) {
return "