org.codehaus.groovy.tools.DocGenerator.groovy Maven / Gradle / Ivy
Show all versions of groovy-all Show documentation
/*
* Copyright 2003-2014 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
import com.thoughtworks.qdox.JavaDocBuilder
import com.thoughtworks.qdox.model.JavaClass
import com.thoughtworks.qdox.model.JavaMethod
import com.thoughtworks.qdox.model.JavaParameter
import com.thoughtworks.qdox.model.Type
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import groovy.text.TemplateEngine
import org.codehaus.groovy.runtime.DefaultGroovyMethods
import org.codehaus.groovy.tools.shell.util.Logger
import org.codehaus.groovy.tools.shell.util.MessageSource
import java.text.BreakIterator
import java.util.concurrent.ConcurrentHashMap
/**
* Generate documentation about the methods provided by the Groovy Development Kit
* that enhance the standard JDK classes.
*
* @author Guillaume Laforge, John Wilson, Bernhard Huber, Paul King, Yasuharu Nakano
*/
class DocGenerator {
private static final MessageSource messages = new MessageSource(DocGenerator)
private static final Logger log = Logger.create(DocGenerator)
private static final Comparator SORT_KEY_COMPARATOR = [compare: { a, b -> return a.sortKey.compareTo(b.sortKey) }] as Comparator
private static final Map CONFIG = new ConcurrentHashMap();
List sourceFiles
File outputDir
DocSource docSource
DocGenerator(List sourceFiles, File outputFolder) {
this.sourceFiles = sourceFiles
this.outputDir = outputFolder
this.docSource = parseSource(sourceFiles)
}
/**
* Parse the DefaultGroovyMethods class to build a graph representing the structure of the class,
* with its methods, javadoc comments and tags.
*/
private static DocSource parseSource(List sourceFiles) {
JavaDocBuilder builder = new JavaDocBuilder()
sourceFiles.each {
if (it.exists()) {
builder.addSource(it.newReader())
log.debug "adding reader for $it"
} else {
log.debug "not found, skipping: $it.path"
}
}
def methods = builder.sources.collectMany { source ->
source.classes.collectMany { aClass ->
aClass.methods.findAll { !it.annotations.any { it.type.fullyQualifiedName == 'java.lang.Deprecated' } }
}
}
def docSource = new DocSource()
methods.each { JavaMethod method ->
if (!method.isPublic() || !method.isStatic()) {
return // skip it
}
def firstParamType = method.parameters[0].type
if (firstParamType.javaClass.fullyQualifiedName.startsWith('groovy')) {
return // nothing, skip it
}
docSource.add(firstParamType, method)
}
docSource.populateInheritedMethods()
return docSource
}
/**
* Builds an HTML page from the structure of DefaultGroovyMethods.
*/
void generateAll() {
def engine = new SimpleTemplateEngine()
// the index.html
def indexTemplate = createTemplate(engine, 'index.html')
new File(outputDir, 'index.html').withWriter {
it << indexTemplate.make(title: CONFIG.title)
}
// the overview-summary.html
def overviewTemplate = createTemplate(engine, 'overview-summary.html')
new File(outputDir, 'overview-summary.html').withWriter {
it << overviewTemplate.make(title: CONFIG.title)
}
// the overview-frame.html
def overviewFrameTemplate = createTemplate(engine, 'template.overview-frame.html')
new File(outputDir, 'overview-frame.html').withWriter {
def docPackagesExceptPrimitiveType = docSource.packages.findAll { !it.primitive }
it << overviewFrameTemplate.make(packages: docPackagesExceptPrimitiveType, title: CONFIG.title)
}
// the package-list
new File(outputDir, 'package-list').withWriter { writer ->
docSource.packages*.name.each { writer.println it }
}
// the allclasses-frame.html
def allClassesTemplate = createTemplate(engine, 'template.allclasses-frame.html')
new File(outputDir, 'allclasses-frame.html').withWriter {
it << allClassesTemplate.make(docTypes: docSource.allDocTypes, title: CONFIG.title)
}
// the package-frame.html and package-summary.html for each package
def packageFrameTemplate = createTemplate(engine, 'template.package-frame.html')
def packageSummaryTemplate = createTemplate(engine, 'template.package-summary.html')
docSource.packages.each { DocPackage docPackage ->
def dir = DocUtil.createPackageDirectory(outputDir, docPackage.name)
new File(dir, 'package-frame.html').withWriter {
it << packageFrameTemplate.make(docPackage: docPackage, title: CONFIG.title)
}
new File(dir, 'package-summary.html').withWriter {
it << packageSummaryTemplate.make(docPackage: docPackage, title: CONFIG.title)
}
}
// the class.html for each class
def classTemplate = createTemplate(engine, 'template.class.html')
docSource.allDocTypes.each { DocType docType ->
def dir = DocUtil.createPackageDirectory(outputDir, docType.packageName)
new File(dir, docType.simpleClassName + '.html').withWriter {
it << classTemplate.make(docType: docType, title: CONFIG.title)
}
}
// the index-all.html
def indexAllTemplate = createTemplate(engine, 'template.index-all.html')
new File(outputDir, 'index-all.html').withWriter {
it << indexAllTemplate.make('indexMap': generateIndexMap(), title: CONFIG.title)
}
// copy resources
['groovy.ico', 'stylesheet.css'].each { String resource ->
new File(outputDir, resource) << getClass().getResource(resource).bytes
}
}
private Template createTemplate(TemplateEngine templateEngine, String resourceFile) {
def resourceUrl = getClass().getResource(resourceFile)
return templateEngine.createTemplate(resourceUrl.text)
}
/**
* Generate an index map for index-all.html.
*
* This method creates a index map indexed by the first letter of the
* method in upper case, the map value is a list of methods.
*
* e.g.: 'A' : [ m1, m2, m3 .. ]
* The values m1, m2, m3 are sorted by the method name, and the parameter signature.
* The method names of m1, m2, m3 start either with 'a', or 'A'.
*
* @return indexMap
*/
private Map generateIndexMap() {
def indexItems = []
docSource.allDocTypes.each { DocType docType ->
// the class
indexItems << [
index: docType.simpleClassName.capitalize()[0],
docType: docType,
sortKey: docType.sortKey,
]
// the methods
docType.docMethods.each { DocMethod docMethod ->
indexItems << [
index: docMethod.javaMethod.name.capitalize()[0],
docType: docType,
docMethod: docMethod,
sortKey: docMethod.sortKey
]
}
}
def indexMap = new TreeMap().withDefault { new TreeSet(SORT_KEY_COMPARATOR) }
for (indexItem in indexItems) {
indexMap[indexItem['index']] << indexItem
}
return indexMap
}
/**
* Main entry point.
*/
static void main(String... args) {
def cli = new CliBuilder(usage : 'DocGenerator [options] [sourcefiles]', posix:false)
cli.help(longOpt: 'help', messages['cli.option.help.description'])
cli._(longOpt: 'version', messages['cli.option.version.description'])
cli.o(longOpt: 'outputDir', args:1, argName: 'path', messages['cli.option.output.dir.description'])
cli.title(longOpt: 'title', args:1, argName: 'text', messages['cli.option.title.description'])
cli.link(args:2, valueSeparator:'=', argName:'comma-separated-package-prefixes=url',
messages['cli.option.link.patterns.description'])
def options = cli.parse(args)
if (options.help) {
cli.usage()
return
}
if (options.links && options.links.size() % 2 == 1) {
throw new IllegalArgumentException("Links should be specified in pattern=url pairs")
}
if (options.version) {
println messages.format('cli.info.version', GroovySystem.version)
return
}
def start = System.currentTimeMillis()
def outputDir = new File(options.outputDir ?: "target/html/groovy-jdk")
outputDir.mkdirs()
CONFIG.title = options.title ?: "Groovy JDK"
if (options.links) {
CONFIG.links = options.links.collate(2).collectMany{ prefixes, url -> prefixes.tokenize(',').collect{[it, url]} }.collectEntries()
}
CONFIG.locale = Locale.default // TODO allow locale to be passed in
def srcFiles = options.arguments().collect { DocUtil.sourceFileOf(it) }
try {
DefaultGroovyMethods.additionals.each { aClass ->
def className = aClass.name.replaceAll(/\$.*/, '')
def additionalFile = DocUtil.sourceFileOf(className)
if (srcFiles.every { it.canonicalPath != additionalFile.canonicalPath }) {
srcFiles << additionalFile
}
}
} catch (MissingPropertyException e) { // TODO is it still needed?
// no call site change available, so ignore it
log.error e.message, e
}
def docGen = new DocGenerator(srcFiles, outputDir)
docGen.generateAll()
def end = System.currentTimeMillis()
log.debug "Done. Took ${end - start} milliseconds."
}
private static class DocSource {
SortedSet packages = new TreeSet(SORT_KEY_COMPARATOR)
void add(Type type, JavaMethod javaMethod) {
DocType tempDocType = new DocType(type: type)
DocPackage aPackage = packages.find { it.name == tempDocType.packageName }
if (!aPackage) {
aPackage = new DocPackage(name: tempDocType.packageName)
packages << aPackage
}
DocType docType = aPackage.docTypes.find { it.fullyQualifiedClassName == tempDocType.fullyQualifiedClassName }
if (!docType) {
docType = tempDocType
aPackage.docTypes << docType
}
def docMethod = new DocMethod(declaringDocType: docType, javaMethod: javaMethod)
docType.docMethods << docMethod
}
void populateInheritedMethods() {
def allTypes = allDocTypes.collectEntries{ [it.fullyQualifiedClassName, it] }
allTypes.each { name, docType ->
if (name.endsWith('[]') || name.startsWith('primitive-types')) return
Type next = docType.javaClass.superClass
while (next != null) {
if (allTypes.keySet().contains(next.value)) {
docType.inheritedMethods[allTypes[next.value]] = allTypes[next.value].docMethods
}
next = next.javaClass.superClass
}
def remaining = docType.javaClass.implementedInterfaces.toList()
while (!remaining.isEmpty()) {
def nextInt = remaining.remove(0)
if (allTypes.keySet().contains(nextInt.fullyQualifiedName)) {
docType.inheritedMethods[allTypes[nextInt.fullyQualifiedName]] = allTypes[nextInt.fullyQualifiedName].docMethods
}
remaining.addAll(nextInt.implementedInterfaces.toList())
}
}
}
SortedSet getAllDocTypes() {
def allSet = new TreeSet(SORT_KEY_COMPARATOR)
allSet.addAll(packages.collectMany { it.docTypes })
return allSet
}
}
private static class DocPackage {
static final String PRIMITIVE_TYPE_PSEUDO_PACKAGE = 'primitive-types'
String name
SortedSet docTypes = new TreeSet(SORT_KEY_COMPARATOR)
boolean isPrimitive() {
name == PRIMITIVE_TYPE_PSEUDO_PACKAGE
}
String getSortKey() {
name
}
}
private static class DocType {
private Type type
final String shortComment = "" // empty because cannot get a comment of JDK
SortedSet docMethods = new TreeSet(SORT_KEY_COMPARATOR)
Map> inheritedMethods = new LinkedHashMap>()
JavaClass getJavaClass() {
type.javaClass
}
String getPackageName() {
if (type.primitive) {
return DocPackage.PRIMITIVE_TYPE_PSEUDO_PACKAGE
}
def fqcn = fullyQualifiedClassName
if (fqcn.indexOf(".") < 0) {
return ""
}
fqcn.replaceAll(/\.[^.]*$/, '')
}
String getSimpleClassName() {
fullyQualifiedClassName.replaceAll(/^.*\./, '')
}
String getFullyQualifiedClassName() {
if (type.primitive) {
return DocPackage.PRIMITIVE_TYPE_PSEUDO_PACKAGE + '.' + type.toString()
}
DocUtil.resolveJdkClassName(type.toString())
}
boolean isInterface() {
type.javaClass.isInterface()
}
String getSortKey() {
simpleClassName + ' ' + fullyQualifiedClassName
}
String linkAnchor(DocType otherDocType) {
DocUtil.getLinkAnchor(otherDocType.fullyQualifiedClassName, packageName)
}
}
private static class DocMethod {
DocType declaringDocType
JavaMethod javaMethod
String getName() {
javaMethod.name
}
/**
* Retrieve the parameters of the method.
*
* @param method a method
* @return a list of parameters without the first one
*/
List getParameters() {
if (javaMethod.getParameters().size() > 1) {
return javaMethod.getParameters().toList()[1..-1]
}
return []
}
String getParametersSignature() {
parameters.collect { DocUtil.resolveJdkClassName(it.type.toString()) }.join(", ")
}
String getParametersDocUrl() {
parameters.collect { "${DocUtil.getLinkAnchor(it.type.toString(), declaringDocType.packageName)} $it.name" }.join(", ")
}
String getReturnTypeDocUrl() {
def returnType = javaMethod.returns
def resolvedReturnType = (returnType) ? DocUtil.resolveJdkClassName(returnType.toString()) : ""
DocUtil.getLinkAnchor(resolvedReturnType, declaringDocType.packageName)
}
String getComment() {
DocUtil.formatJavadocText(javaMethod.comment ?: '', declaringDocType.packageName)
}
String getShortComment() {
DocUtil.formatJavadocText(DocUtil.getFirstSentence(javaMethod.comment ?: ''), declaringDocType.packageName)
}
String getReturnComment() {
DocUtil.formatJavadocText(javaMethod.getTagByName("return")?.value ?: '', declaringDocType.packageName)
}
Map getParameterComments() {
javaMethod.getTagsByName("param").drop(1).collectEntries { // first arg is the "real this"
def name = it.value.replaceAll(/ .*/, '')
def comment = DocUtil.formatJavadocText(it.value.replaceAll(/^\w*/, ''), declaringDocType.packageName)
[name, comment]
}
}
List getSeeComments() {
javaMethod.getTagsByName("see").collect { DocUtil.getLinkAnchor(it.value, declaringDocType.packageName) }
}
String getSinceComment() {
javaMethod.getTagByName("since")?.value
}
boolean isStatic() {
javaMethod.parentClass.name == 'DefaultGroovyStaticMethods'
}
String getSortKey() {
name + ' ' + parametersSignature + ' ' + declaringDocType.fullyQualifiedClassName
}
}
private static class DocUtil {
static String resolveJdkClassName(String className) {
if (className in ['T', 'E', 'U', 'K', 'V', 'G']) {
return 'java.lang.Object'
}
if (className in ['T[]', 'E[]']) {
return 'java.lang.Object[]'
}
return className
}
static String formatJavadocText(String text, String packageName) {
linkify(codify(text), packageName)
}
private static String linkify(String text, String packageName) {
text.replaceAll(/\{@link\s+([^}]*)\s*\}/) { String all, String destination ->
// A class name cannot be omitted: https://jira.codehaus.org/browse/GROOVY-6740 TODO: remove DocUtil once fixed?
DocUtil.getLinkAnchor(destination, packageName)
}
}
private static String codify(String text) {
text.replaceAll(/\{@code\s+([^}]*)\s*\}/) { String all, String code -> """${code}
""" }
}
static String getFirstSentence(String text) {
def boundary = BreakIterator.getSentenceInstance(CONFIG.locale)
boundary.setText(text)
int start = boundary.first()
int end = boundary.next()
if (start > -1 && end > -1) {
return text.substring(start, end)
}
return text
}
static String getLinkAnchor(String destination, String originPackageName) {
// resolving a destination if it's in GDK.
def inGdk = destination.startsWith('#')
if (inGdk) {
(destination =~ /#([^(]*)\(([^)]+)\)/).each { String all, String name, String argsText ->
def args = argsText.split(/,\s?/).toList()
def first = args.remove(0)
destination = "$first#$name(${args.join(', ')})".toString()
}
}
def fullyQualifiedClassName = resolveJdkClassName(destination.replaceFirst(/#.*$/, ''))
def methodSignatureHash = destination.replaceFirst(/^[^#]*/, '')
def simpleClassName = fullyQualifiedClassName.replaceFirst(/.*\./, "")
def packageName = fullyQualifiedClassName.replaceFirst(/.?[^.]+$/, '')
// If a package is empty, a destination text should be just returned
// because a link to the right documentation location cannot be made.
if (packageName.empty) {
return destination
}
def apiBaseUrl, title
if (inGdk) {
apiBaseUrl = '../' * (originPackageName.count('.') + 1)
title = "GDK enhancement for ${fullyQualifiedClassName}"
} else {
title = "Class in $packageName"
apiBaseUrl = './'
String key = CONFIG.links.keySet().find{ packageName.startsWith(it) }
if (key) {
apiBaseUrl = CONFIG.links[key]
if (apiBaseUrl.startsWith('..')) apiBaseUrl = '../' * (originPackageName.count('.') + 1) + apiBaseUrl
}
}
def url = "${apiBaseUrl}${packageName.replace('.', '/')}/${simpleClassName}.html${methodSignatureHash}"
return """${simpleClassName}${methodSignatureHash}"""
}
static File createPackageDirectory(File outputDir, String packageName) {
def packagePath = filePathOf(packageName)
def dir = new File(outputDir, packagePath)
dir.mkdirs()
return dir
}
private static String filePathOf(String packageName) {
def fileSep = File.separator
// need to escape separator on windows for regex's sake
if (fileSep == '\\') fileSep *= 2
return packageName.replaceAll(/\./, fileSep)
}
static File sourceFileOf(String pathOrClassName) {
// TODO don't hardcode like this
if (pathOrClassName.contains("/")) {
return new File(pathOrClassName)
}
new File("src/main/" + pathOrClassName.replace('.', '/') + ".java")
}
}
}