| /* |
| * 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; |
| } |
| } |
| } |