/*
* Copyright 2003-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.tools.groovydoc;
import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;
import org.codehaus.groovy.antlr.AntlrASTProcessor;
import org.codehaus.groovy.antlr.SourceBuffer;
import org.codehaus.groovy.antlr.UnicodeEscapingReader;
import org.codehaus.groovy.antlr.java.Groovifier;
import org.codehaus.groovy.antlr.java.Java2GroovyConverter;
import org.codehaus.groovy.antlr.java.JavaLexer;
import org.codehaus.groovy.antlr.java.JavaRecognizer;
import org.codehaus.groovy.antlr.parser.GroovyLexer;
import org.codehaus.groovy.antlr.parser.GroovyRecognizer;
import org.codehaus.groovy.antlr.treewalker.PreOrderTraversal;
import org.codehaus.groovy.antlr.treewalker.SourceCodeTraversal;
import org.codehaus.groovy.antlr.treewalker.Visitor;
import org.codehaus.groovy.groovydoc.GroovyClassDoc;
import org.codehaus.groovy.groovydoc.GroovyRootDoc;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.tools.shell.util.Logger;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*
* todo: order methods alphabetically (implement compareTo enough?)
*/
public class GroovyRootDocBuilder {
private final Logger log = Logger.create(GroovyRootDocBuilder.class);
private static final char FS = '/';
private List<LinkArgument> links;
private final GroovyDocTool tool;
private final String[] sourcepaths;
private final SimpleGroovyRootDoc rootDoc;
private final Properties properties;
public GroovyRootDocBuilder(GroovyDocTool tool, String[] sourcepaths, List<LinkArgument> links, Properties properties) {
this.tool = tool;
this.sourcepaths = sourcepaths;
this.links = links;
this.rootDoc = new SimpleGroovyRootDoc("root");
this.properties = properties;
}
// parsing
public Map<String, GroovyClassDoc> getClassDocsFromSingleSource(String packagePath, String file, String src)
throws RecognitionException, TokenStreamException {
if (file.indexOf(".java") > 0) { // simple (for now) decision on java or groovy
// java
return parseJava(packagePath, file, src);
}
if (file.indexOf(".sourcefile") > 0) {
// java (special name used for testing)
return parseJava(packagePath, file, src);
}
// not java, try groovy instead :-)
return parseGroovy(packagePath, file, src);
}
private Map<String, GroovyClassDoc> parseJava(String packagePath, String file, String src)
throws RecognitionException, TokenStreamException {
SourceBuffer sourceBuffer = new SourceBuffer();
JavaRecognizer parser = getJavaParser(src, sourceBuffer);
String[] tokenNames = parser.getTokenNames();
try {
parser.compilationUnit();
} catch (OutOfMemoryError e) {
log.error("Out of memory while processing: " + packagePath + "/" + file);
throw e;
}
AST ast = parser.getAST();
// modify the Java AST into a Groovy AST (just token types)
Visitor java2groovyConverter = new Java2GroovyConverter(tokenNames);
AntlrASTProcessor java2groovyTraverser = new PreOrderTraversal(java2groovyConverter);
java2groovyTraverser.process(ast);
// now mutate (groovify) the ast into groovy
Visitor groovifier = new Groovifier(tokenNames, false);
AntlrASTProcessor groovifierTraverser = new PreOrderTraversal(groovifier);
groovifierTraverser.process(ast);
// now do the business
Visitor visitor = new SimpleGroovyClassDocAssembler(packagePath, file, sourceBuffer, links, properties, false);
AntlrASTProcessor traverser = new SourceCodeTraversal(visitor);
traverser.process(ast);
return ((SimpleGroovyClassDocAssembler) visitor).getGroovyClassDocs();
}
private Map<String, GroovyClassDoc> parseGroovy(String packagePath, String file, String src)
throws RecognitionException, TokenStreamException {
SourceBuffer sourceBuffer = new SourceBuffer();
GroovyRecognizer parser = getGroovyParser(src, sourceBuffer);
try {
parser.compilationUnit();
} catch (OutOfMemoryError e) {
log.error("Out of memory while processing: " + packagePath + "/" + file);
throw e;
}
AST ast = parser.getAST();
// now do the business
Visitor visitor = new SimpleGroovyClassDocAssembler(packagePath, file, sourceBuffer, links, properties, true);
AntlrASTProcessor traverser = new SourceCodeTraversal(visitor);
traverser.process(ast);
return ((SimpleGroovyClassDocAssembler) visitor).getGroovyClassDocs();
}
private JavaRecognizer getJavaParser(String input, SourceBuffer sourceBuffer) {
UnicodeEscapingReader unicodeReader = new UnicodeEscapingReader(new StringReader(input), sourceBuffer);
JavaLexer lexer = new JavaLexer(unicodeReader);
unicodeReader.setLexer(lexer);
JavaRecognizer parser = JavaRecognizer.make(lexer);
parser.setSourceBuffer(sourceBuffer);
return parser;
}
private GroovyRecognizer getGroovyParser(String input, SourceBuffer sourceBuffer) {
UnicodeEscapingReader unicodeReader = new UnicodeEscapingReader(new StringReader(input), sourceBuffer);
GroovyLexer lexer = new GroovyLexer(unicodeReader);
unicodeReader.setLexer(lexer);
GroovyRecognizer parser = GroovyRecognizer.make(lexer);
parser.setSourceBuffer(sourceBuffer);
return parser;
}
public void buildTree(List<String> filenames) throws IOException, RecognitionException, TokenStreamException {
setOverview();
List<File> sourcepathFiles = new ArrayList<File>();
if (sourcepaths != null) {
for (String sourcepath : sourcepaths) {
sourcepathFiles.add(new File(sourcepath).getAbsoluteFile());
}
}
for (String filename : filenames) {
File srcFile = new File(filename);
if (srcFile.exists()) {
processFile(filename, srcFile, true);
continue;
}
for (File spath : sourcepathFiles) {
srcFile = new File(spath, filename);
if (srcFile.exists()) {
processFile(filename, srcFile, false);
break;
}
}
}
}
private void setOverview() {
String path = properties.getProperty("overviewFile");
if (path != null && path.length() > 0) {
try {
String content = DefaultGroovyMethods.getText(new File(path));
calcThenSetOverviewDescription(content);
} catch (IOException e) {
System.err.println("Unable to load overview file: " + e.getMessage());
}
}
}
private void processFile(String filename, File srcFile, boolean isAbsolute) throws IOException {
String src = DefaultGroovyMethods.getText(srcFile);
String packagePath = isAbsolute ? "DefaultPackage" : tool.getPath(filename).replace('\\', FS);
String file = tool.getFile(filename);
SimpleGroovyPackageDoc packageDoc = null;
if (!isAbsolute) {
packageDoc = (SimpleGroovyPackageDoc) rootDoc.packageNamed(packagePath);
}
// todo: this might not work correctly for absolute paths
if (filename.endsWith("package.html") || filename.endsWith("package-info.java") || filename.endsWith("package-info.groovy")) {
if (packageDoc == null) {
packageDoc = new SimpleGroovyPackageDoc(packagePath);
}
processPackageInfo(src, filename, packageDoc);
rootDoc.put(packagePath, packageDoc);
return;
}
try {
Map<String, GroovyClassDoc> classDocs = getClassDocsFromSingleSource(packagePath, file, src);
rootDoc.putAllClasses(classDocs);
if (isAbsolute) {
Iterator<Map.Entry<String, GroovyClassDoc>> iterator = classDocs.entrySet().iterator();
if (iterator.hasNext()) {
final Map.Entry<String, GroovyClassDoc> docEntry = iterator.next();
String fullPath = docEntry.getValue().getFullPathName();
int slash = fullPath.lastIndexOf(FS);
if (slash > 0) packagePath = fullPath.substring(0, slash);
packageDoc = (SimpleGroovyPackageDoc) rootDoc.packageNamed(packagePath);
}
}
if (packageDoc == null) {
packageDoc = new SimpleGroovyPackageDoc(packagePath);
}
packageDoc.putAll(classDocs);
rootDoc.put(packagePath, packageDoc);
} catch (RecognitionException e) {
log.error("ignored due to RecognitionException: " + filename + " [" + e.getMessage() + "]");
log.debug("ignored due to RecognitionException: " + filename + " [" + e.getMessage() + "]", e);
} catch (TokenStreamException e) {
log.error("ignored due to TokenStreamException: " + filename + " [" + e.getMessage() + "]");
log.debug("ignored due to TokenStreamException: " + filename + " [" + e.getMessage() + "]", e);
}
}
/* package private */ void processPackageInfo(String src, String filename, SimpleGroovyPackageDoc packageDoc) {
String description = calcThenSetPackageDescription(src, filename, packageDoc);
calcThenSetSummary(description, packageDoc);
}
private String calcThenSetPackageDescription(String src, String filename, SimpleGroovyPackageDoc packageDoc) {
String description;
if (filename.endsWith(".html")) {
description = scrubOffExcessiveTags(src);
description = pruneTagFromFront(description, "p");
description = pruneTagFromEnd(description, "/p");
} else {
description = trimPackageAndComments(src);
}
description = replaceTags(description, packageDoc);
packageDoc.setDescription(description);
return description;
}
// TODO remove dup with SimpleGroovyClassDoc
private String replaceTags(String orig, SimpleGroovyPackageDoc packageDoc) {
String result = orig.replaceAll("(?m)^\\s*\\*", ""); // todo precompile regex
// {@link processing hack}
result = replaceAllTags(result, "", "", SimpleGroovyClassDoc.LINK_REGEX, packageDoc);
// {@code processing hack}
result = replaceAllTags(result, "<TT>", "</TT>", SimpleGroovyClassDoc.CODE_REGEX, packageDoc);
// hack to reformat other groovydoc block tags (@see, @return, @param, @throws, @author, @since) into html
result = replaceAllTags(result + "@endMarker", "<DL><DT><B>$1:</B></DT><DD>", "</DD></DL>", SimpleGroovyClassDoc.TAG_REGEX, packageDoc);
// remove @endMarker
result = result.substring(0, result.length() - 10);
return SimpleGroovyClassDoc.decodeSpecialSymbols(result);
}
// TODO remove dup with SimpleGroovyClassDoc
private String replaceAllTags(String self, String s1, String s2, Pattern regex, SimpleGroovyPackageDoc packageDoc) {
Matcher matcher = regex.matcher(self);
if (matcher.find()) {
matcher.reset();
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String tagname = matcher.group(1);
if (tagname.equals("see") || tagname.equals("link")) {
matcher.appendReplacement(sb, s1 + SimpleGroovyClassDoc.getDocUrl(
SimpleGroovyClassDoc.encodeSpecialSymbols(matcher.group(2)), false, links, packageDoc.getRelativeRootPath(), rootDoc, null) + s2);
} else if (!tagname.equals("interface")) {
matcher.appendReplacement(sb, s1 + SimpleGroovyClassDoc.encodeSpecialSymbols(matcher.group(2)) + s2);
}
}
matcher.appendTail(sb);
return sb.toString();
} else {
return self;
}
}
private void calcThenSetSummary(String src, SimpleGroovyPackageDoc packageDoc) {
packageDoc.setSummary(SimpleGroovyDoc.calculateFirstSentence(src));
}
private void calcThenSetOverviewDescription(String src) {
String description = scrubOffExcessiveTags(src);
rootDoc.setDescription(description);
}
private String trimPackageAndComments(String src) {
return src.replaceFirst("(?sm)^package.*", "")
.replaceFirst("(?sm)/.*\\*\\*(.*)\\*/", "$1")
.replaceAll("(?m)^\\s*\\*", "");
}
private String scrubOffExcessiveTags(String src) {
String description = pruneTagFromFront(src, "html");
description = pruneTagFromFront(description, "/head");
description = pruneTagFromFront(description, "body");
description = pruneTagFromEnd(description, "/html");
return pruneTagFromEnd(description, "/body");
}
private String pruneTagFromFront(String description, String tag) {
int index = Math.max(indexOfTag(description, tag.toLowerCase()), indexOfTag(description, tag.toUpperCase()));
if (index < 0) return description;
return description.substring(index);
}
private String pruneTagFromEnd(String description, String tag) {
int index = Math.max(description.lastIndexOf("<" + tag.toLowerCase() + ">"),
description.lastIndexOf("<" + tag.toUpperCase() + ">"));
if (index < 0) return description;
return description.substring(0, index);
}
private int indexOfTag(String text, String tag) {
int pos = text.indexOf("<" + tag + ">");
if (pos > 0) pos += tag.length() + 2;
return pos;
}
public GroovyRootDoc getRootDoc() {
rootDoc.resolve();
return rootDoc;
}
}