blob: 1ed6032602fea3f890c400a019787b162832af47 [file] [log] [blame]
/*
* Copyright 2006 Google Inc.
*
* 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 com.google.doctool;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
/**
* Orchestrates the behavior of {@link Booklet}, {@link SplitterJoiner} and
* other tools to create user documentation and API documentation.
*/
public class DocTool {
private class ImageCopier extends DefaultHandler {
private final File htmlDir;
private ImageCopier(File htmlDir) {
this.htmlDir = htmlDir;
}
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
if (qName.equalsIgnoreCase("img")) {
String imgSrc = attributes.getValue("src");
if (imgSrc != null) {
boolean found = false;
for (int i = 0, n = imagePath.length; i < n; ++i) {
File dir = imagePath[i];
File inFile = new File(dir, imgSrc);
if (inFile.exists()) {
// Copy it over.
//
found = true;
File outFile = new File(htmlDir, imgSrc);
if (outFile.exists()) {
if (outFile.lastModified() > inFile.lastModified()) {
// Already up to date.
break;
}
} else {
File outFileDir = outFile.getParentFile();
if (!outFileDir.exists() && !outFileDir.mkdirs()) {
err.println("Unable to create image output dir " + outFileDir);
break;
}
}
if (!copyFile(inFile, outFile)) {
err.println("Unable to copy image file " + outFile);
}
}
}
if (!found) {
err.println("Unable to find image " + imgSrc);
}
}
}
}
}
private static final Pattern IN_XML_FILENAME = Pattern.compile(
"(.+)\\.([^\\.]+)\\.xml", Pattern.CASE_INSENSITIVE);
public static void main(String[] args) {
DocToolFactory factory = new DocToolFactory();
String arg;
String pathSep = System.getProperty("path.separator");
for (int i = 0, n = args.length; i < n; ++i) {
if (tryParseFlag(args, i, "-help")) {
printHelp();
return;
} else if (null != (arg = tryParseArg(args, i, "-out"))) {
++i;
factory.setOutDir(arg);
} else if (null != (arg = tryParseArg(args, i, "-html"))) {
++i;
factory.setGenerateHtml(true);
factory.setTitle(arg);
// Slurp every arg not prefixed with "-".
for (; i + 1 < n && !args[i + 1].startsWith("-"); ++i) {
factory.addHtmlFileBase(args[i + 1]);
}
} else if (null != (arg = tryParseArg(args, i, "-overview"))) {
++i;
factory.setOverviewFile(arg);
} else if (null != (arg = tryParseArg(args, i, "-sourcepath"))) {
++i;
String[] entries = arg.split("\\" + pathSep);
for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
factory.addToSourcePath(entries[entryIndex]);
}
} else if (null != (arg = tryParseArg(args, i, "-classpath"))) {
++i;
String[] entries = arg.split("\\" + pathSep);
for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
factory.addToClassPath(entries[entryIndex]);
}
} else if (null != (arg = tryParseArg(args, i, "-packages"))) {
++i;
String[] entries = arg.split("\\" + pathSep);
for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
factory.addToPackages(entries[entryIndex]);
}
} else if (null != (arg = tryParseArg(args, i, "-imagepath"))) {
++i;
String[] entries = arg.split("\\" + pathSep);
for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
factory.addToImagePath(entries[entryIndex]);
}
} else {
if (factory.getFileType() == null) {
factory.setFileType(args[i]);
} else {
factory.setFileBase(args[i]);
}
}
}
DocTool docTool = factory.create(System.out, System.err);
if (docTool != null) {
docTool.process();
}
}
public static boolean recursiveDelete(File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (int i = 0; i < children.length; i++) {
if (!recursiveDelete(children[i])) {
return false;
}
}
}
}
if (!file.delete()) {
System.err.println("Unable to delete " + file.getAbsolutePath());
return false;
}
return true;
}
private static void printHelp() {
String s = "";
s += "DocTool (filetype filebase)? [docset-creation-options] [html-creation-options]\n";
s += " Creates structured javadoc xml output from Java source and/or\n";
s += " a table of contents and a set of cross-linked html files.\n";
s += " Specifying filebase/filetype produces output file \"filebase.filetype.xml\".\n";
s += " Specifying -html produces output files in ${out}/html.\n";
s += "\n";
s += "[docset-creation-options] are\n";
s += " -out\n";
s += " The output directory\n";
s += " -overview\n";
s += " The overview html file for this doc set\n";
s += " -sourcepath path\n";
s += " The path to find Java source for this doc set\n";
s += " -classpath path\n";
s += " The path to find imported classes for this doc set\n";
s += " -packages package-names\n";
s += " The command-separated list of package names to include in this doc set\n";
s += "\n";
s += "[html-creation-options] are\n";
s += " -html title filebase+\n";
s += " Causes topics in the named filebase(s) to be merged and converted into html\n";
s += " -imagepath\n";
s += " The semicolon-separated path to find images for html\n";
System.out.println(s);
}
/**
* Parse a flag with a argument.
*/
private static String tryParseArg(String[] args, int i, String name) {
if (i < args.length) {
if (args[i].equals(name)) {
if (i + 1 < args.length) {
String arg = args[i + 1];
if (arg.startsWith("-")) {
System.out.println("Warning: arg to " + name
+ " looks more like a flag: " + arg);
}
return arg;
} else {
throw new IllegalArgumentException("Expecting an argument after "
+ name);
}
}
}
return null;
}
/**
* Parse just a flag with no subsequent argument.
*/
private static boolean tryParseFlag(String[] args, int i, String name) {
if (i < args.length) {
if (args[i].equals(name)) {
return true;
}
}
return false;
}
private final File[] classPath;
private final String[] packages;
private final PrintStream err;
private final String base;
private final String fileType;
private final boolean generateHtml;
private final String[] htmlFileBases;
private final File[] imagePath;
private final PrintStream out;
private final File outDir;
private final File overviewFile;
private final File[] sourcePath;
private final String title;
DocTool(PrintStream out, PrintStream err, File outDir, boolean generateHtml,
String title, String[] htmlFileBases, String fileType, String fileBase,
File overviewFile, File[] sourcePath, File[] classPath,
String[] packages, File[] imagePath) {
this.out = out;
this.err = err;
this.outDir = outDir;
this.generateHtml = generateHtml;
this.base = fileBase;
this.fileType = fileType;
this.overviewFile = overviewFile;
this.sourcePath = sourcePath;
this.classPath = classPath;
this.packages = packages;
this.imagePath = imagePath;
this.title = title;
this.htmlFileBases = htmlFileBases.clone();
}
public boolean copyFile(File in, File out) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(in);
fos = new FileOutputStream(out);
byte[] buf = new byte[4096];
int i = 0;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
return true;
} catch (IOException e) {
return false;
} finally {
close(fis);
close(fos);
}
}
private void close(InputStream is) {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace(err);
}
}
}
private void close(OutputStream os) {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace(err);
}
}
}
private boolean copyImages(File htmlDir, File mergedTopicsFile) {
FileReader fileReader = null;
Throwable caught = null;
try {
fileReader = new FileReader(mergedTopicsFile);
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
InputSource inputSource = new InputSource(fileReader);
XMLReader xmlReader = parser.getXMLReader();
xmlReader.setContentHandler(new ImageCopier(htmlDir));
xmlReader.parse(inputSource);
return true;
} catch (SAXException e) {
caught = e;
Exception inner = e.getException();
if (inner != null) {
caught = inner;
}
} catch (ParserConfigurationException e) {
caught = e;
} catch (IOException e) {
caught = e;
} finally {
try {
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace(err);
}
}
caught.printStackTrace(err);
return false;
}
private Set findSourcePackages() {
Set results = new HashSet();
for (int i = 0, n = sourcePath.length; i < n; ++i) {
File srcDir = sourcePath[i];
findSourcePackages(results, srcDir, "");
}
return results;
}
private void findSourcePackages(Set results, File dir, String parentPackage) {
File[] children = dir.listFiles();
if (children != null) {
for (int i = 0, n = children.length; i < n; ++i) {
File child = children[i];
String childName = parentPackage
+ (parentPackage.length() > 0 ? "." : "") + child.getName();
if (child.isDirectory()) {
// Recurse
findSourcePackages(results, child, childName);
} else if (child.getName().endsWith(".java")) {
// Only include this dir as a result if there's at least one java file
results.add(parentPackage);
}
}
}
}
private String flattenPath(File[] entries) {
String pathSep = System.getProperty("path.separator");
String path = "";
for (int i = 0, n = entries.length; i < n; ++i) {
File entry = entries[i];
if (i > 0) {
path += pathSep;
}
path += entry.getAbsolutePath();
}
return path;
}
private void freshenIf(File file) {
if (!file.isFile()) {
return;
}
String name = file.getName();
Matcher matcher = IN_XML_FILENAME.matcher(name);
if (matcher.matches()) {
String suffix = "." + matcher.group(2) + ".xml";
File topicFile = tryReplaceSuffix(file, suffix, ".topics.xml");
if (topicFile != null) {
if (file.lastModified() > topicFile.lastModified()) {
String xsltFileName = matcher.group(2) + "-" + "topics.xslt";
String xslt = getFileFromClassPath(xsltFileName); // yucky slow
out.println(file + " -> " + topicFile);
transform(xslt, file, topicFile, null);
}
}
}
}
private boolean genHtml() {
// Make sure the html directory exists.
//
File htmlDir = new File(outDir, "html");
if (!htmlDir.exists() && !htmlDir.mkdirs()) {
err.println("Cannot create html output directory "
+ htmlDir.getAbsolutePath());
return false;
}
// Merge all *.topics.xml into one topics.xml file.
//
File mergedTopicsFile = new File(outDir, "topics.xml");
if (!mergeTopics(mergedTopicsFile)) {
return false;
}
// Parse it all to find the images and copy them over.
//
copyImages(htmlDir, mergedTopicsFile);
// Transform to merged topics into merged htmls.
//
File mergedHtmlsFile = new File(htmlDir, "topics.htmls");
long lastModifiedHtmls = mergedHtmlsFile.lastModified();
long lastModifiedTopics = mergedTopicsFile.lastModified();
if (!mergedHtmlsFile.exists() || lastModifiedHtmls < lastModifiedTopics) {
String xsltHtmls = getFileFromClassPath("topics-htmls.xslt");
Map params = new HashMap();
params.put("title", title);
transform(xsltHtmls, mergedTopicsFile, mergedHtmlsFile, params);
// Split the merged htmls into many html files.
//
if (!splitHtmls(mergedHtmlsFile)) {
return false;
}
// Create a table of contents.
//
File tocFile = new File(htmlDir, "contents.html");
String xsltToc = getFileFromClassPath("topics-toc.xslt");
transform(xsltToc, mergedTopicsFile, tocFile, params);
// Copy the CSS file over.
//
String css = getFileFromClassPath("doc.css");
try {
FileWriter cssWriter = new FileWriter(new File(htmlDir, "doc.css"));
cssWriter.write(css);
cssWriter.close();
} catch (IOException e) {
e.printStackTrace(err);
}
} else {
out.println("Skipping html creation since nothing seems to have changed since "
+ mergedHtmlsFile.getAbsolutePath());
}
return true;
}
private String getFileFromClassPath(String filename) {
InputStream in = null;
try {
in = getClass().getClassLoader().getResourceAsStream(filename);
try {
if (in == null) {
throw new RuntimeException("Cannot find file: " + filename);
}
StringWriter sw = new StringWriter();
int ch;
while ((ch = in.read()) != -1) {
sw.write(ch);
}
return sw.toString();
} finally {
if (in != null) {
in.close();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean mergeTopics(File mergedTopicsFile) {
try {
List args = new ArrayList();
args.add("join"); // what to do
args.add("topics"); // the outer element is <topics>
args.add(mergedTopicsFile.getAbsolutePath());
// For each of the htmlFileBases, try to find a file having that name to
// merge into the big topics doc.
//
boolean foundAny = false;
for (int i = 0, n = htmlFileBases.length; i < n; ++i) {
String filebase = htmlFileBases[i];
File fileToMerge = new File(outDir, filebase + ".topics.xml");
if (fileToMerge.exists()) {
foundAny = true;
args.add(fileToMerge.getAbsolutePath());
} else {
err.println("Unable to find " + fileToMerge.getName());
}
}
if (foundAny) {
String[] argArray = (String[]) args.toArray(new String[0]);
traceCommand("SplitterJoiner", argArray);
SplitterJoiner.main(argArray);
} else {
err.println("No topics found");
return false;
}
} catch (IOException e) {
e.printStackTrace(err);
return false;
}
return true;
}
/**
* Runs the help process.
*/
private boolean process() {
if (fileType != null) {
// Produce XML from JavaDoc.
//
String fileName = base + "." + fileType + ".xml";
if (!runBooklet(new File(outDir, fileName))) {
return false;
}
}
// Process existing files to get them into topics format.
// Done afterwards for convenience when debugging your doc.
//
transformExistingIntoTopicXml();
if (generateHtml) {
// Merge into HTML.
if (!genHtml()) {
return false;
}
}
return true;
}
private boolean runBooklet(File bkoutFile) {
// Write out the list of packages that can be found on the source path.
out.println("Creating " + bkoutFile.getAbsolutePath());
Set srcPackages = findSourcePackages();
if (srcPackages.isEmpty()) {
err.println("No input files found");
return false;
}
List args = new ArrayList();
// For now, harded-coded, but could be passed through
args.add("-source");
args.add("1.5");
// The doclet
args.add("-doclet");
args.add(Booklet.class.getName());
// Class path
args.add("-classpath");
args.add(flattenPath(classPath));
// Source path
args.add("-sourcepath");
args.add(flattenPath(sourcePath));
// Encoding is always UTF-8
args.add("-encoding");
args.add("UTF-8");
// Overview file
if (overviewFile != null) {
args.add("-overview");
args.add(overviewFile.getAbsolutePath());
}
// Output file
args.add("-bkout");
args.add(bkoutFile.getAbsolutePath());
if (packages != null) {
// Specify the packages to actually emit doc for
StringBuffer bkdocpkg = new StringBuffer();
for (int i = 0; i < packages.length; i++) {
String pkg = packages[i];
bkdocpkg.append(pkg);
bkdocpkg.append(";");
}
args.add("-bkdocpkg");
args.add(bkdocpkg.toString());
}
args.add("-breakiterator");
// Specify the set of input packages (needed by JavaDoc)
args.addAll(srcPackages);
String[] argArray = (String[]) args.toArray(new String[0]);
traceCommand("Booklet", argArray);
Booklet.main(argArray);
return bkoutFile.exists();
}
private boolean splitHtmls(File mergedHtmlsFile) {
try {
List args = new ArrayList();
args.add("split"); // what to do
args.add(mergedHtmlsFile.getAbsolutePath());
String[] argArray = (String[]) args.toArray(new String[0]);
traceCommand("SplitterJoiner", argArray);
SplitterJoiner.main(argArray);
} catch (IOException e) {
e.printStackTrace(err);
return false;
}
return true;
}
private void traceCommand(String cmd, String[] args) {
out.print(cmd);
for (int i = 0, n = args.length; i < n; ++i) {
String arg = args[i];
out.print(" ");
out.print(arg);
}
out.println();
}
private void transform(String xslt, File inFile, File outFile, Map params) {
Throwable caught = null;
try {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
StreamSource xsltSource = new StreamSource(new StringReader(xslt));
Transformer transformer = transformerFactory.newTransformer(xsltSource);
transformer.setOutputProperty(
javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT,
"yes");
transformer.setOutputProperty(
"{http://xml.apache.org/xslt}indent-amount", "4");
if (params != null) {
for (Iterator iter = params.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
transformer.setParameter((String) entry.getKey(), entry.getValue());
}
}
FileOutputStream fos = new FileOutputStream(outFile);
StreamResult result = new StreamResult(fos);
StreamSource xmlSource = new StreamSource(new FileReader(inFile));
transformer.transform(xmlSource, result);
fos.close();
return;
} catch (TransformerConfigurationException e) {
caught = e;
} catch (TransformerException e) {
caught = e;
} catch (IOException e) {
caught = e;
}
throw new RuntimeException("Unable to complete the xslt tranform", caught);
}
private void transformExistingIntoTopicXml() {
File[] children = outDir.listFiles();
if (children != null) {
for (int i = 0, n = children.length; i < n; ++i) {
File file = children[i];
freshenIf(file);
}
}
}
private File tryReplaceSuffix(File file, String oldSuffix, String newSuffix) {
String name = file.getName();
if (name.endsWith(oldSuffix)) {
String baseName = name.substring(0, name.length() - oldSuffix.length());
return new File(file.getParent(), baseName + newSuffix);
} else {
return null;
}
}
}