001: /*
002: * Copyright 2006 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.doctool;
017:
018: import org.xml.sax.Attributes;
019: import org.xml.sax.InputSource;
020: import org.xml.sax.SAXException;
021: import org.xml.sax.XMLReader;
022: import org.xml.sax.helpers.DefaultHandler;
023:
024: import java.io.File;
025: import java.io.FileInputStream;
026: import java.io.FileOutputStream;
027: import java.io.FileReader;
028: import java.io.FileWriter;
029: import java.io.IOException;
030: import java.io.InputStream;
031: import java.io.OutputStream;
032: import java.io.PrintStream;
033: import java.io.StringReader;
034: import java.io.StringWriter;
035: import java.util.ArrayList;
036: import java.util.HashMap;
037: import java.util.HashSet;
038: import java.util.Iterator;
039: import java.util.List;
040: import java.util.Map;
041: import java.util.Set;
042: import java.util.regex.Matcher;
043: import java.util.regex.Pattern;
044:
045: import javax.xml.parsers.ParserConfigurationException;
046: import javax.xml.parsers.SAXParser;
047: import javax.xml.parsers.SAXParserFactory;
048: import javax.xml.transform.Transformer;
049: import javax.xml.transform.TransformerConfigurationException;
050: import javax.xml.transform.TransformerException;
051: import javax.xml.transform.TransformerFactory;
052: import javax.xml.transform.stream.StreamResult;
053: import javax.xml.transform.stream.StreamSource;
054:
055: /**
056: * Orchestrates the behavior of {@link Booklet}, {@link SplitterJoiner} and
057: * other tools to create user documentation and API documentation.
058: */
059: public class DocTool {
060:
061: private class ImageCopier extends DefaultHandler {
062:
063: private final File htmlDir;
064:
065: private ImageCopier(File htmlDir) {
066: this .htmlDir = htmlDir;
067: }
068:
069: public void startElement(String uri, String localName,
070: String qName, Attributes attributes)
071: throws SAXException {
072: if (qName.equalsIgnoreCase("img")) {
073: String imgSrc = attributes.getValue("src");
074: if (imgSrc != null) {
075: boolean found = false;
076: for (int i = 0, n = imagePath.length; i < n; ++i) {
077: File dir = imagePath[i];
078: File inFile = new File(dir, imgSrc);
079: if (inFile.exists()) {
080: // Copy it over.
081: //
082: found = true;
083: File outFile = new File(htmlDir, imgSrc);
084:
085: if (outFile.exists()) {
086: if (outFile.lastModified() > inFile
087: .lastModified()) {
088: // Already up to date.
089: break;
090: }
091: } else {
092: File outFileDir = outFile
093: .getParentFile();
094: if (!outFileDir.exists()
095: && !outFileDir.mkdirs()) {
096: err
097: .println("Unable to create image output dir "
098: + outFileDir);
099: break;
100: }
101: }
102: if (!copyFile(inFile, outFile)) {
103: err
104: .println("Unable to copy image file "
105: + outFile);
106: }
107: }
108: }
109: if (!found) {
110: err.println("Unable to find image " + imgSrc);
111: }
112: }
113: }
114: }
115: }
116:
117: private static final Pattern IN_XML_FILENAME = Pattern.compile(
118: "(.+)\\.([^\\.]+)\\.xml", Pattern.CASE_INSENSITIVE);
119:
120: public static void main(String[] args) {
121: DocToolFactory factory = new DocToolFactory();
122: String arg;
123: String pathSep = System.getProperty("path.separator");
124: for (int i = 0, n = args.length; i < n; ++i) {
125: if (tryParseFlag(args, i, "-help")) {
126: printHelp();
127: return;
128: } else if (null != (arg = tryParseArg(args, i, "-out"))) {
129: ++i;
130: factory.setOutDir(arg);
131: } else if (null != (arg = tryParseArg(args, i, "-html"))) {
132: ++i;
133: factory.setGenerateHtml(true);
134: factory.setTitle(arg);
135:
136: // Slurp every arg not prefixed with "-".
137: for (; i + 1 < n && !args[i + 1].startsWith("-"); ++i) {
138: factory.addHtmlFileBase(args[i + 1]);
139: }
140: } else if (null != (arg = tryParseArg(args, i, "-overview"))) {
141: ++i;
142: factory.setOverviewFile(arg);
143: } else if (null != (arg = tryParseArg(args, i,
144: "-sourcepath"))) {
145: ++i;
146: String[] entries = arg.split("\\" + pathSep);
147: for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
148: factory.addToSourcePath(entries[entryIndex]);
149: }
150: } else if (null != (arg = tryParseArg(args, i, "-classpath"))) {
151: ++i;
152: String[] entries = arg.split("\\" + pathSep);
153: for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
154: factory.addToClassPath(entries[entryIndex]);
155: }
156: } else if (null != (arg = tryParseArg(args, i, "-packages"))) {
157: ++i;
158: String[] entries = arg.split("\\" + pathSep);
159: for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
160: factory.addToPackages(entries[entryIndex]);
161: }
162: } else if (null != (arg = tryParseArg(args, i, "-imagepath"))) {
163: ++i;
164: String[] entries = arg.split("\\" + pathSep);
165: for (int entryIndex = 0; entryIndex < entries.length; entryIndex++) {
166: factory.addToImagePath(entries[entryIndex]);
167: }
168: } else {
169: if (factory.getFileType() == null) {
170: factory.setFileType(args[i]);
171: } else {
172: factory.setFileBase(args[i]);
173: }
174: }
175: }
176:
177: DocTool docTool = factory.create(System.out, System.err);
178: if (docTool != null) {
179: docTool.process();
180: }
181: }
182:
183: public static boolean recursiveDelete(File file) {
184: if (file.isDirectory()) {
185: File[] children = file.listFiles();
186: if (children != null) {
187: for (int i = 0; i < children.length; i++) {
188: if (!recursiveDelete(children[i])) {
189: return false;
190: }
191: }
192: }
193: }
194: if (!file.delete()) {
195: System.err.println("Unable to delete "
196: + file.getAbsolutePath());
197: return false;
198: }
199: return true;
200: }
201:
202: private static void printHelp() {
203: String s = "";
204: s += "DocTool (filetype filebase)? [docset-creation-options] [html-creation-options]\n";
205: s += " Creates structured javadoc xml output from Java source and/or\n";
206: s += " a table of contents and a set of cross-linked html files.\n";
207: s += " Specifying filebase/filetype produces output file \"filebase.filetype.xml\".\n";
208: s += " Specifying -html produces output files in ${out}/html.\n";
209: s += "\n";
210: s += "[docset-creation-options] are\n";
211: s += " -out\n";
212: s += " The output directory\n";
213: s += " -overview\n";
214: s += " The overview html file for this doc set\n";
215: s += " -sourcepath path\n";
216: s += " The path to find Java source for this doc set\n";
217: s += " -classpath path\n";
218: s += " The path to find imported classes for this doc set\n";
219: s += " -packages package-names\n";
220: s += " The command-separated list of package names to include in this doc set\n";
221: s += "\n";
222: s += "[html-creation-options] are\n";
223: s += " -html title filebase+\n";
224: s += " Causes topics in the named filebase(s) to be merged and converted into html\n";
225: s += " -imagepath\n";
226: s += " The semicolon-separated path to find images for html\n";
227: System.out.println(s);
228: }
229:
230: /**
231: * Parse a flag with a argument.
232: */
233: private static String tryParseArg(String[] args, int i, String name) {
234: if (i < args.length) {
235: if (args[i].equals(name)) {
236: if (i + 1 < args.length) {
237: String arg = args[i + 1];
238: if (arg.startsWith("-")) {
239: System.out.println("Warning: arg to " + name
240: + " looks more like a flag: " + arg);
241: }
242: return arg;
243: } else {
244: throw new IllegalArgumentException(
245: "Expecting an argument after " + name);
246: }
247: }
248: }
249: return null;
250: }
251:
252: /**
253: * Parse just a flag with no subsequent argument.
254: */
255: private static boolean tryParseFlag(String[] args, int i,
256: String name) {
257: if (i < args.length) {
258: if (args[i].equals(name)) {
259: return true;
260: }
261: }
262: return false;
263: }
264:
265: private final File[] classPath;
266:
267: private final String[] packages;
268:
269: private final PrintStream err;
270:
271: private final String base;
272:
273: private final String fileType;
274:
275: private final boolean generateHtml;
276:
277: private final String[] htmlFileBases;
278:
279: private final File[] imagePath;
280:
281: private final PrintStream out;
282:
283: private final File outDir;
284:
285: private final File overviewFile;
286:
287: private final File[] sourcePath;
288:
289: private final String title;
290:
291: DocTool(PrintStream out, PrintStream err, File outDir,
292: boolean generateHtml, String title, String[] htmlFileBases,
293: String fileType, String fileBase, File overviewFile,
294: File[] sourcePath, File[] classPath, String[] packages,
295: File[] imagePath) {
296: this .out = out;
297: this .err = err;
298: this .outDir = outDir;
299: this .generateHtml = generateHtml;
300: this .base = fileBase;
301: this .fileType = fileType;
302: this .overviewFile = overviewFile;
303: this .sourcePath = sourcePath;
304: this .classPath = classPath;
305: this .packages = packages;
306: this .imagePath = imagePath;
307: this .title = title;
308: this .htmlFileBases = (String[]) htmlFileBases.clone();
309: }
310:
311: public boolean copyFile(File in, File out) {
312: FileInputStream fis = null;
313: FileOutputStream fos = null;
314: try {
315: fis = new FileInputStream(in);
316: fos = new FileOutputStream(out);
317: byte[] buf = new byte[4096];
318: int i = 0;
319: while ((i = fis.read(buf)) != -1) {
320: fos.write(buf, 0, i);
321: }
322: return true;
323: } catch (Exception e) {
324: return false;
325: } finally {
326: close(fis);
327: close(fos);
328: }
329: }
330:
331: private void close(InputStream is) {
332: if (is != null) {
333: try {
334: is.close();
335: } catch (IOException e) {
336: e.printStackTrace(err);
337: }
338: }
339: }
340:
341: private void close(OutputStream os) {
342: if (os != null) {
343: try {
344: os.close();
345: } catch (IOException e) {
346: e.printStackTrace(err);
347: }
348: }
349: }
350:
351: private boolean copyImages(File htmlDir, File mergedTopicsFile) {
352: FileReader fileReader = null;
353: Throwable caught = null;
354: try {
355: fileReader = new FileReader(mergedTopicsFile);
356: SAXParser parser = SAXParserFactory.newInstance()
357: .newSAXParser();
358: InputSource inputSource = new InputSource(fileReader);
359: XMLReader xmlReader = parser.getXMLReader();
360: xmlReader.setContentHandler(new ImageCopier(htmlDir));
361: xmlReader.parse(inputSource);
362: return true;
363: } catch (SAXException e) {
364: caught = e;
365: Exception inner = e.getException();
366: if (inner != null) {
367: caught = inner;
368: }
369: } catch (ParserConfigurationException e) {
370: caught = e;
371: } catch (IOException e) {
372: caught = e;
373: } finally {
374: try {
375: if (fileReader != null) {
376: fileReader.close();
377: }
378: } catch (IOException e) {
379: e.printStackTrace(err);
380: }
381: }
382: caught.printStackTrace(err);
383: return false;
384: }
385:
386: private Set findSourcePackages() {
387: Set results = new HashSet();
388: for (int i = 0, n = sourcePath.length; i < n; ++i) {
389: File srcDir = sourcePath[i];
390: findSourcePackages(results, srcDir, "");
391: }
392: return results;
393: }
394:
395: private void findSourcePackages(Set results, File dir,
396: String parentPackage) {
397: File[] children = dir.listFiles();
398: if (children != null) {
399: for (int i = 0, n = children.length; i < n; ++i) {
400: File child = children[i];
401: String childName = parentPackage
402: + (parentPackage.length() > 0 ? "." : "")
403: + child.getName();
404: if (child.isDirectory()) {
405: // Recurse
406: findSourcePackages(results, child, childName);
407: } else if (child.getName().endsWith(".java")) {
408: // Only include this dir as a result if there's at least one java file
409: results.add(parentPackage);
410: }
411: }
412: }
413: }
414:
415: private String flattenPath(File[] entries) {
416: String pathSep = System.getProperty("path.separator");
417: String path = "";
418: for (int i = 0, n = entries.length; i < n; ++i) {
419: File entry = entries[i];
420: if (i > 0) {
421: path += pathSep;
422: }
423: path += entry.getAbsolutePath();
424: }
425: return path;
426: }
427:
428: private void freshenIf(File file) {
429: if (!file.isFile()) {
430: return;
431: }
432:
433: String name = file.getName();
434: Matcher matcher = IN_XML_FILENAME.matcher(name);
435: if (matcher.matches()) {
436: String suffix = "." + matcher.group(2) + ".xml";
437: File topicFile = tryReplaceSuffix(file, suffix,
438: ".topics.xml");
439: if (topicFile != null) {
440: if (file.lastModified() > topicFile.lastModified()) {
441: String xsltFileName = matcher.group(2) + "-"
442: + "topics.xslt";
443: String xslt = getFileFromClassPath(xsltFileName); // yucky slow
444: out.println(file + " -> " + topicFile);
445: transform(xslt, file, topicFile, null);
446: }
447: }
448: }
449: }
450:
451: private boolean genHtml() {
452: // Make sure the html directory exists.
453: //
454: File htmlDir = new File(outDir, "html");
455: if (!htmlDir.exists() && !htmlDir.mkdirs()) {
456: err.println("Cannot create html output directory "
457: + htmlDir.getAbsolutePath());
458: return false;
459: }
460:
461: // Merge all *.topics.xml into one topics.xml file.
462: //
463: File mergedTopicsFile = new File(outDir, "topics.xml");
464: if (!mergeTopics(mergedTopicsFile)) {
465: return false;
466: }
467:
468: // Parse it all to find the images and copy them over.
469: //
470: copyImages(htmlDir, mergedTopicsFile);
471:
472: // Transform to merged topics into merged htmls.
473: //
474: File mergedHtmlsFile = new File(htmlDir, "topics.htmls");
475: long lastModifiedHtmls = mergedHtmlsFile.lastModified();
476: long lastModifiedTopics = mergedTopicsFile.lastModified();
477: if (!mergedHtmlsFile.exists()
478: || lastModifiedHtmls < lastModifiedTopics) {
479: String xsltHtmls = getFileFromClassPath("topics-htmls.xslt");
480:
481: Map params = new HashMap();
482: params.put("title", title);
483:
484: transform(xsltHtmls, mergedTopicsFile, mergedHtmlsFile,
485: params);
486:
487: // Split the merged htmls into many html files.
488: //
489: if (!splitHtmls(mergedHtmlsFile)) {
490: return false;
491: }
492:
493: // Create a table of contents.
494: //
495: File tocFile = new File(htmlDir, "contents.html");
496: String xsltToc = getFileFromClassPath("topics-toc.xslt");
497: transform(xsltToc, mergedTopicsFile, tocFile, params);
498:
499: // Copy the CSS file over.
500: //
501: String css = getFileFromClassPath("doc.css");
502: try {
503: FileWriter cssWriter = new FileWriter(new File(htmlDir,
504: "doc.css"));
505: cssWriter.write(css);
506: cssWriter.close();
507: } catch (IOException e) {
508: e.printStackTrace(err);
509: }
510: } else {
511: out
512: .println("Skipping html creation since nothing seems to have changed since "
513: + mergedHtmlsFile.getAbsolutePath());
514: }
515:
516: return true;
517: }
518:
519: private String getFileFromClassPath(String filename) {
520: InputStream in = null;
521: try {
522: in = getClass().getClassLoader().getResourceAsStream(
523: filename);
524: try {
525: if (in == null) {
526: err.println("Cannot find file: " + filename);
527: System.exit(-1); // yuck
528: }
529: StringWriter sw = new StringWriter();
530: int ch;
531: while ((ch = in.read()) != -1) {
532: sw.write(ch);
533: }
534: return sw.toString();
535: } finally {
536: if (in != null) {
537: in.close();
538: }
539: }
540: } catch (IOException e) {
541: throw new RuntimeException(e);
542: }
543: }
544:
545: private boolean mergeTopics(File mergedTopicsFile) {
546: try {
547: List args = new ArrayList();
548: args.add("join"); // what to do
549: args.add("topics"); // the outer element is <topics>
550: args.add(mergedTopicsFile.getAbsolutePath());
551:
552: // For each of the htmlFileBases, try to find a file having that name to
553: // merge into the big topics doc.
554: //
555: boolean foundAny = false;
556: for (int i = 0, n = htmlFileBases.length; i < n; ++i) {
557: String filebase = htmlFileBases[i];
558: File fileToMerge = new File(outDir, filebase
559: + ".topics.xml");
560: if (fileToMerge.exists()) {
561: foundAny = true;
562: args.add(fileToMerge.getAbsolutePath());
563: } else {
564: err.println("Unable to find "
565: + fileToMerge.getName());
566: }
567: }
568:
569: if (foundAny) {
570: String[] argArray = (String[]) args
571: .toArray(new String[0]);
572: traceCommand("SplitterJoiner", argArray);
573: SplitterJoiner.main(argArray);
574: } else {
575: err.println("No topics found");
576: return false;
577: }
578: } catch (IOException e) {
579: e.printStackTrace(err);
580: return false;
581: }
582: return true;
583: }
584:
585: /**
586: * Runs the help process.
587: */
588: private boolean process() {
589: if (fileType != null) {
590: // Produce XML from JavaDoc.
591: //
592: String fileName = base + "." + fileType + ".xml";
593: if (!runBooklet(new File(outDir, fileName))) {
594: return false;
595: }
596: }
597:
598: // Process existing files to get them into topics format.
599: // Done afterwards for convenience when debugging your doc.
600: //
601: transformExistingIntoTopicXml();
602:
603: if (generateHtml) {
604: // Merge into HTML.
605: if (!genHtml()) {
606: return false;
607: }
608: }
609:
610: return true;
611: }
612:
613: private boolean runBooklet(File bkoutFile) {
614: // Write out the list of packages that can be found on the source path.
615: out.println("Creating " + bkoutFile.getAbsolutePath());
616: Set srcPackages = findSourcePackages();
617: if (srcPackages.isEmpty()) {
618: err.println("No input files found");
619: return false;
620: }
621:
622: List args = new ArrayList();
623:
624: // For now, harded-coded, but could be passed through
625: args.add("-source");
626: args.add("1.5");
627:
628: // The doclet
629: args.add("-doclet");
630: args.add(Booklet.class.getName());
631:
632: // Class path
633: args.add("-classpath");
634: args.add(flattenPath(classPath));
635:
636: // Source path
637: args.add("-sourcepath");
638: args.add(flattenPath(sourcePath));
639:
640: // Encoding is always UTF-8
641: args.add("-encoding");
642: args.add("UTF-8");
643:
644: // Overview file
645: if (overviewFile != null) {
646: args.add("-overview");
647: args.add(overviewFile.getAbsolutePath());
648: }
649:
650: // Output file
651: args.add("-bkout");
652: args.add(bkoutFile.getAbsolutePath());
653:
654: if (packages != null) {
655: // Specify the packages to actually emit doc for
656: StringBuffer bkdocpkg = new StringBuffer();
657: for (int i = 0; i < packages.length; i++) {
658: String pkg = packages[i];
659: bkdocpkg.append(pkg);
660: bkdocpkg.append(";");
661: }
662: args.add("-bkdocpkg");
663: args.add(bkdocpkg.toString());
664: }
665:
666: args.add("-breakiterator");
667:
668: // Specify the set of input packages (needed by JavaDoc)
669: args.addAll(srcPackages);
670:
671: String[] argArray = (String[]) args.toArray(new String[0]);
672: traceCommand("Booklet", argArray);
673: Booklet.main(argArray);
674:
675: return bkoutFile.exists();
676: }
677:
678: private boolean splitHtmls(File mergedHtmlsFile) {
679: try {
680: List args = new ArrayList();
681: args.add("split"); // what to do
682: args.add(mergedHtmlsFile.getAbsolutePath());
683: String[] argArray = (String[]) args.toArray(new String[0]);
684: traceCommand("SplitterJoiner", argArray);
685: SplitterJoiner.main(argArray);
686: } catch (IOException e) {
687: e.printStackTrace(err);
688: return false;
689: }
690: return true;
691: }
692:
693: private void traceCommand(String cmd, String[] args) {
694: out.print(cmd);
695: for (int i = 0, n = args.length; i < n; ++i) {
696: String arg = args[i];
697: out.print(" ");
698: out.print(arg);
699: }
700: out.println();
701: }
702:
703: private void transform(String xslt, File inFile, File outFile,
704: Map params) {
705: Throwable caught = null;
706: try {
707: TransformerFactory transformerFactory = TransformerFactory
708: .newInstance();
709: StreamSource xsltSource = new StreamSource(
710: new StringReader(xslt));
711: Transformer transformer = transformerFactory
712: .newTransformer(xsltSource);
713: transformer
714: .setOutputProperty(
715: javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION,
716: "yes");
717: transformer.setOutputProperty(
718: javax.xml.transform.OutputKeys.INDENT, "yes");
719: transformer.setOutputProperty(
720: "{http://xml.apache.org/xslt}indent-amount", "4");
721:
722: if (params != null) {
723: for (Iterator iter = params.entrySet().iterator(); iter
724: .hasNext();) {
725: Map.Entry entry = (Map.Entry) iter.next();
726: transformer.setParameter((String) entry.getKey(),
727: entry.getValue());
728: }
729: }
730:
731: FileOutputStream fos = new FileOutputStream(outFile);
732: StreamResult result = new StreamResult(fos);
733: StreamSource xmlSource = new StreamSource(new FileReader(
734: inFile));
735: transformer.transform(xmlSource, result);
736: fos.close();
737: return;
738: } catch (TransformerConfigurationException e) {
739: caught = e;
740: } catch (TransformerException e) {
741: caught = e;
742: } catch (IOException e) {
743: caught = e;
744: }
745: throw new RuntimeException(
746: "Unable to complete the xslt tranform", caught);
747: }
748:
749: private void transformExistingIntoTopicXml() {
750: File[] children = outDir.listFiles();
751: if (children != null) {
752: for (int i = 0, n = children.length; i < n; ++i) {
753: File file = children[i];
754: freshenIf(file);
755: }
756: }
757: }
758:
759: private File tryReplaceSuffix(File file, String oldSuffix,
760: String newSuffix) {
761: String name = file.getName();
762: if (name.endsWith(oldSuffix)) {
763: String baseName = name.substring(0, name.length()
764: - oldSuffix.length());
765: return new File(file.getParent(), baseName + newSuffix);
766: } else {
767: return null;
768: }
769: }
770: }
|