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