001: /*BEGIN_COPYRIGHT_BLOCK
002: *
003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are met:
008: * * Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * * Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
014: * names of its contributors may be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: *
029: * This software is Open Source Initiative approved Open Source Software.
030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
031: *
032: * This file is part of DrJava. Download the current version of this project
033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
034: *
035: * END_COPYRIGHT_BLOCK*/
036:
037: package edu.rice.cs.drjava.model.javadoc;
038:
039: import java.io.File;
040: import java.io.IOException;
041: import java.io.BufferedReader;
042: import java.io.StringReader;
043: import java.util.ArrayList;
044: import java.util.List;
045: import java.util.Collection;
046: import java.util.Properties;
047:
048: import edu.rice.cs.plt.lambda.Thunk;
049: import edu.rice.cs.plt.io.IOUtil;
050: import edu.rice.cs.plt.concurrent.ConcurrentUtil;
051: import edu.rice.cs.plt.iter.IterUtil;
052: import edu.rice.cs.plt.text.TextUtil;
053: import edu.rice.cs.util.ArgumentTokenizer;
054: import edu.rice.cs.util.DirectorySelector;
055: import edu.rice.cs.util.FileOpenSelector;
056: import edu.rice.cs.drjava.model.FileSaveSelector;
057: import edu.rice.cs.drjava.model.OpenDefinitionsDocument;
058: import edu.rice.cs.drjava.model.GlobalModel;
059: import edu.rice.cs.drjava.model.FileMovedException;
060: import edu.rice.cs.drjava.model.definitions.InvalidPackageException;
061: import edu.rice.cs.util.OperationCanceledException;
062: import edu.rice.cs.drjava.DrJava;
063: import edu.rice.cs.drjava.config.Configuration;
064: import edu.rice.cs.drjava.config.OptionConstants;
065: import edu.rice.cs.drjava.config.FileOption;
066: import edu.rice.cs.drjava.model.compiler.CompilerErrorModel;
067: import edu.rice.cs.drjava.model.compiler.CompilerError;
068:
069: import static edu.rice.cs.plt.debug.DebugUtil.error;
070:
071: /** Default implementation of JavadocModel interface; generates Javadoc HTML files for a set of documents.
072: * @version $Id: DefaultJavadocModel.java 4255 2007-08-28 19:17:37Z mgricken $
073: */
074: public class DefaultJavadocModel implements JavadocModel {
075:
076: /** Used by CompilerErrorModel to open documents that have errors. */
077: private GlobalModel _model;
078:
079: /**Manages listeners to this model. */
080: private final JavadocEventNotifier _notifier = new JavadocEventNotifier();
081:
082: /** Location of the java command to use (if not the default in {@code java.home}) */
083: private final File _javaCommand;
084:
085: /** Location of the tools library containing the javadoc code (if not on the javaCommand's boot class path) */
086: private final Iterable<File> _toolsPath;
087:
088: /** The error model containing all current Javadoc errors. */
089: private CompilerErrorModel _javadocErrorModel;
090:
091: /**
092: * Main constructor.
093: * @param model Source of documents for this JavadocModel
094: * @param javaCommand Location of the java command to use (if not the default in {@code java.home})
095: * @param toolsPath Location of the tools library containing the javadoc code (if not on the
096: * javaCommand's boot class path)
097: */
098: public DefaultJavadocModel(GlobalModel model, File javaCommand,
099: Iterable<File> toolsPath) {
100: _model = model;
101: _javaCommand = javaCommand;
102: _toolsPath = toolsPath;
103: _javadocErrorModel = new CompilerErrorModel();
104: }
105:
106: public boolean isAvailable() {
107: return true;
108: }
109:
110: //-------------------------- Listener Management --------------------------//
111:
112: /** Add a JavadocListener to the model.
113: * @param listener a listener that reacts to Javadoc events
114: */
115: public void addListener(JavadocListener listener) {
116: _notifier.addListener(listener);
117: }
118:
119: /** Remove a JavadocListener from the model. If the listener is not currently
120: * listening to this model, this method has no effect.
121: * @param listener a listener that reacts to Javadoc events
122: */
123: public void removeListener(JavadocListener listener) {
124: _notifier.removeListener(listener);
125: }
126:
127: /** Removes all JavadocListeners from this model. */
128: public void removeAllListeners() {
129: _notifier.removeAllListeners();
130: }
131:
132: //----------------------------- Error Results -----------------------------//
133:
134: /** Accessor for the Javadoc error model.
135: * @return the CompilerErrorModel managing Javadoc errors.
136: */
137: public CompilerErrorModel getJavadocErrorModel() {
138: return _javadocErrorModel;
139: }
140:
141: /** Clears all current Javadoc errors. */
142: public void resetJavadocErrors() {
143: _javadocErrorModel = new CompilerErrorModel();
144: }
145:
146: // -------------------- Javadoc All Documents --------------------
147:
148: /** Javadocs all open documents, after ensuring that all are saved. The user provides a destination, and the global
149: * model provides the package info. Must run in the event-handling thread.
150: * @param select a command object for selecting a directory and warning a user about bad input
151: * @param saver a command object for saving a document (if it moved/changed)
152: * @param classPath a collection of classpath elements to be used by Javadoc
153: * @throws IOException if there is a problem manipulating files
154: */
155: public void javadocAll(DirectorySelector select,
156: final FileSaveSelector saver) throws IOException {
157:
158: /* Only javadoc if all are saved. Removed because it is already done inside suggestJavadocDestination; fixes bug
159: where pop-up is shown twice) */
160: if (_model.hasModifiedDocuments()
161: || _model.hasUntitledDocuments()) {
162: return;
163: } /* abort if files remain unsaved */
164:
165: Configuration config = DrJava.getConfig();
166: File destDir = config
167: .getSetting(OptionConstants.JAVADOC_DESTINATION);
168:
169: // Get the destination directory via the DirectorySelector, if appropriate.
170: try {
171: if (destDir.equals(FileOption.NULL_FILE)) {
172: /* This is the default, stock behavior of a new install. If no destination is set, don't pass
173: anything to the ui command. Let the command object decide what to do. */
174: destDir = select.getDirectory(null);
175: } else
176: // Otherwise, tell the command object to prefer the config's default.
177: destDir = select.getDirectory(destDir);
178:
179: // Make sure the destination is usable.
180: while (!destDir.exists() || !destDir.isDirectory()
181: || !destDir.canWrite()) {
182: if (!destDir.getPath().equals("") && !destDir.exists()) {
183: // If the choice doesn't exist, ask to create it.
184: boolean create = select
185: .askUser(
186: "The directory you chose does not exist:\n'"
187: + destDir
188: + "'\nWould you like to create it?",
189: "Create Directory?");
190: if (create) {
191: boolean dirMade = destDir.mkdirs();
192: if (!dirMade)
193: throw new IOException(
194: "Could not create directory: "
195: + destDir);
196: } else
197: return;
198: } else if (!destDir.isDirectory()
199: || destDir.getPath().equals("")) {
200: // We can't use it if it isn't a directory
201: select.warnUser(
202: "The file you chose is not a directory:\n"
203: + "'" + destDir + "'\n"
204: + "Please choose another.",
205: "Not a Directory!");
206: destDir = select.getDirectory(null);
207: } else {
208: //If the directory isn't writable, tell the user and ask again.
209: select
210: .warnUser(
211: "The directory you chose is not writable:\n"
212: + "'"
213: + destDir
214: + "'\n"
215: + "Please choose another directory.",
216: "Cannot Write to Destination!");
217: destDir = select.getDirectory(null);
218: }
219: }
220: } catch (OperationCanceledException oce) {
221: return;
222: } // If the user cancels anywhere, silently return.
223:
224: _notifier.javadocStarted(); // fire first so _javadocAllWorker can fire javadocEnded
225: // Start a new thread to do the work.
226: final File destDirF = destDir;
227: new Thread("DrJava Javadoc Thread") {
228: public void run() {
229: _javadocAllWorker(destDirF, saver);
230: }
231: }.start();
232: }
233:
234: /** This method handles most of the logic of performing a Javadoc operation, once we know that it won't be canceled.
235: * @param destDirFile the destination directory for the doc files
236: * @param saver a command object for saving a document (if it moved/changed)
237: * @param classpath an array of classpath elements to be used by Javadoc
238: */
239: private void _javadocAllWorker(File destDirFile,
240: FileSaveSelector saver) {
241: // Note: JAVADOC_FROM_ROOTS is intended to set the -subpackages flag, but I don't think that's something
242: // we should support -- in general, we only support performing operations on the files that are open.
243:
244: List<String> docFiles = new ArrayList<String>(); // files to send to Javadoc
245:
246: for (OpenDefinitionsDocument doc : _model
247: .getOpenDefinitionsDocuments()) {
248: try {
249: // This will throw an IllegalStateException if no file can be found
250: File file = _getFileFromDocument(doc, saver);
251: docFiles.add(file.getPath());
252: } catch (IllegalStateException e) {
253: // Something wrong with _getFileFromDocument; ignore
254: } catch (IOException ioe) {
255: // can't access file; ignore
256: }
257: }
258:
259: // Don't attempt to create Javadoc if no files are open, or if open file is unnamed.
260: if (docFiles.size() == 0)
261: return;
262:
263: // Run the actual Javadoc process
264: _runJavadoc(docFiles, destDirFile, IterUtil.<String> empty(),
265: false);
266: }
267:
268: // -------------------- Javadoc Current Document --------------------
269:
270: /** Generates Javadoc for the given document only, after ensuring it is saved. Saves the output in a temp directory
271: * which is passed to _javadocDocuemntWorker, which is passed to a subsequent javadocEnded event.
272: * @param doc Document to generate Javadoc for
273: * @param saver a command object for saving the document (if it moved/changed)
274: * @param classPath a collection of classpath elements to be used by Javadoc
275: *
276: * @throws IOException if there is a problem manipulating files
277: */
278: public void javadocDocument(final OpenDefinitionsDocument doc,
279: final FileSaveSelector saver) throws IOException {
280: // Prompt to save if necessary
281: // (TO DO: should only need to save the current document)
282: if (doc.isUntitled() || doc.isModifiedSinceSave())
283: _notifier.saveBeforeJavadoc();
284:
285: // Make sure it is saved
286: if (doc.isUntitled() || doc.isModifiedSinceSave())
287: return; // The user didn't save, so don't generate Javadoc
288:
289: // Try to get the file from the document
290: final File file = _getFileFromDocument(doc, saver);
291:
292: // Generate to a temporary directory
293: final File destDir = IOUtil.createAndMarkTempDirectory(
294: "DrJava-javadoc", "");
295:
296: _notifier.javadocStarted(); // fire first so _javadocDocumntWorker can fire javadocEnded
297: // Start a new thread to do the work.
298: new Thread("DrJava Javadoc Thread") {
299: public void run() {
300: Iterable<String> extraArgs = IterUtil.make("-noindex",
301: "-notree", "-nohelp", "-nonavbar");
302: _runJavadoc(IterUtil.make(file.getPath()), destDir,
303: extraArgs, true);
304: }
305: }.start();
306: }
307:
308: // -------------------- Helper Methods --------------------
309:
310: /** Suggests a default location for generating Javadoc, based on the given document's source root. (Appends
311: * JavadocModel.SUGGESTED_DIR_NAME to the sourceroot.) Ensures that the document is saved first, or else no
312: * reasonable suggestion will be found.
313: * @param doc Document with the source root to use as the default.
314: * @return Suggested destination directory, or null if none could be determined.
315: */
316: public File suggestJavadocDestination(OpenDefinitionsDocument doc) {
317: _attemptSaveAllDocuments();
318:
319: try {
320: File sourceRoot = doc.getSourceRoot();
321: return new File(sourceRoot, SUGGESTED_DIR_NAME);
322: } catch (InvalidPackageException ipe) {
323: return null;
324: }
325: }
326:
327: /**
328: * If any documents are modified, this gives the user a chance
329: * to save them before proceeding.
330: *
331: * Callers can check _getter.hasModifiedDocuments() after calling
332: * this method to determine if the user cancelled the save process.
333: */
334: private void _attemptSaveAllDocuments() {
335: // Only javadoc if all are saved.
336: if (_model.hasModifiedDocuments()
337: || _model.hasUntitledDocuments())
338: _notifier.saveBeforeJavadoc();
339: }
340:
341: /**
342: * Run a new process to generate javdocs, and then tell the listeners when we're done.
343: *
344: * @param files List of files to generate
345: * @param destDir Directory where the results are being saved
346: * @param extraArgs List of additional arguments to use with javadoc (besides those gathered from config settings)
347: * @param allDocs Whether this is running on all documents
348: */
349: private void _runJavadoc(Iterable<String> files, File destDir,
350: Iterable<String> extraArgs, boolean allDocs) {
351: Iterable<String> args = IterUtil.empty();
352: args = IterUtil.compose(args, IterUtil.make("-d", destDir
353: .getPath()));
354: args = IterUtil.compose(args, IterUtil.make("-classpath",
355: IOUtil.pathToString(_model.getClassPath())));
356: args = IterUtil.compose(args, _getLinkArgs());
357: args = IterUtil.compose(args, "-"
358: + DrJava.getConfig().getSetting(
359: OptionConstants.JAVADOC_ACCESS_LEVEL));
360: args = IterUtil.compose(args, extraArgs);
361: String custom = DrJava.getConfig().getSetting(
362: OptionConstants.JAVADOC_CUSTOM_PARAMS);
363: args = IterUtil.compose(args, ArgumentTokenizer
364: .tokenize(custom));
365: args = IterUtil.compose(args, files);
366:
367: File javaCommand = (_javaCommand == null) ? new File(System
368: .getProperty("java.home", "")) : _javaCommand;
369: Iterable<File> jvmClassPath = (_toolsPath == null) ? IterUtil
370: .<File> empty() : _toolsPath;
371:
372: List<CompilerError> errors = new ArrayList<CompilerError>();
373: try {
374: Process p = ConcurrentUtil.runJavaProcess(javaCommand,
375: "com.sun.tools.javadoc.Main", args, jvmClassPath,
376: new File(System.getProperty("user.dir", "")),
377: new Properties(), IterUtil.<String> empty());
378: Thunk<String> outputString = ConcurrentUtil
379: .processOutAsString(p);
380: Thunk<String> errorString = ConcurrentUtil
381: .processErrAsString(p);
382: p.waitFor();
383: errors.addAll(_extractErrors(outputString.value()));
384: errors.addAll(_extractErrors(errorString.value()));
385: } catch (IOException e) {
386: errors.add(new CompilerError("IOException: "
387: + e.getMessage(), false));
388: } catch (InterruptedException e) {
389: errors.add(new CompilerError("InterruptedException: "
390: + e.getMessage(), false));
391: }
392:
393: _javadocErrorModel = new CompilerErrorModel(IterUtil.asArray(
394: errors, CompilerError.class), _model);
395:
396: // waitFor() exit value is 1 for both errors and warnings, so it's no use
397: boolean success = _javadocErrorModel.hasOnlyWarnings();
398: if (success && !allDocs) {
399: IOUtil.deleteOnExitRecursively(destDir);
400: }
401: _notifier.javadocEnded(success, destDir, allDocs);
402: }
403:
404: private Iterable<String> _getLinkArgs() {
405: Configuration config = DrJava.getConfig();
406: String linkVersion = config
407: .getSetting(OptionConstants.JAVADOC_LINK_VERSION);
408: if (linkVersion.equals(OptionConstants.JAVADOC_1_3_TEXT)) {
409: return IterUtil.make("-link", config
410: .getSetting(OptionConstants.JAVADOC_1_3_LINK));
411: } else if (linkVersion.equals(OptionConstants.JAVADOC_1_4_TEXT)) {
412: return IterUtil.make("-link", config
413: .getSetting(OptionConstants.JAVADOC_1_4_LINK));
414: } else if (linkVersion.equals(OptionConstants.JAVADOC_1_5_TEXT)) {
415: return IterUtil.make("-link", config
416: .getSetting(OptionConstants.JAVADOC_1_5_LINK));
417: } else {
418: // should never happen -- use an enum to guarantee
419: return IterUtil.empty();
420: }
421: }
422:
423: /**
424: * Reads through javadoc output text, looking for Javadoc errors. This code will detect Exceptions and
425: * Errors thrown during generation of the output, as well as errors and warnings generated by Javadoc.
426: * This code works for both JDK 1.3 and 1.4, assuming you pass in data from the correct stream. (Be safe
427: * and check both.)
428: */
429: private List<CompilerError> _extractErrors(String text) {
430: BufferedReader r = new BufferedReader(new StringReader(text));
431: List<CompilerError> result = new ArrayList<CompilerError>();
432:
433: String[] errorIndicators = new String[] { "Error: ",
434: "Exception: ", "invalid flag:" };
435:
436: try {
437: String output = r.readLine();
438: while (output != null) {
439: if (TextUtil.containsAny(output, errorIndicators)) {
440: // If we found one, put the remaining stream contents in one CompilerError.
441: result.add(new CompilerError(output + '\n'
442: + IOUtil.toString(r), false));
443: } else {
444: // Otherwise, parser for a normal error message.
445: CompilerError error = _parseJavadocErrorLine(output);
446: if (error != null) {
447: result.add(error);
448: }
449: }
450: output = r.readLine();
451: }
452: } catch (IOException e) {
453: error.log(e); /* should not happen, since we're reading from a string */
454: }
455:
456: return result;
457: }
458:
459: /** Convert a line of Javadoc text to a CompilerError. If unable to do so, returns {@code null}. */
460: private CompilerError _parseJavadocErrorLine(String line) {
461: int errStart = line.indexOf(".java:");
462: if (errStart == -1) {
463: return null; /* ignore the line if it doesn't have file info */
464: }
465: // filename is everything up to and including the '.java'
466: String fileName = line.substring(0, errStart + 5);
467:
468: // line number is all contiguous number characters after the colon
469: int lineno = -1;
470: final StringBuilder linenoString = new StringBuilder();
471: int pos = errStart + 6;
472: while ((line.charAt(pos) >= '0') && (line.charAt(pos) <= '9')) {
473: linenoString.append(line.charAt(pos));
474: pos++;
475: }
476: // Hopefully, there is a colon after the line number but before the error message.
477: // If so, record the line number.
478: // Otherwise, try to recover by just using everything after ERR_INDICATOR as the error message
479: if (line.charAt(pos) == ':') {
480: try {
481: // Adjust Javadoc's one-based line numbers to our zero-based indeces.
482: lineno = Integer.valueOf(linenoString.toString())
483: .intValue() - 1;
484: } catch (NumberFormatException e) {
485: }
486: } else {
487: pos = errStart;
488: }
489:
490: // error message is everything after the colon and space that are after the line number
491: String errMessage = line.substring(pos + 2);
492:
493: // check to see if the first word in the error message is "warning"
494: boolean isWarning = false;
495: if (errMessage.substring(0, 7).equalsIgnoreCase("warning")) {
496: isWarning = true;
497: }
498:
499: if (lineno >= 0) {
500: return new CompilerError(new File(fileName), lineno, 0,
501: errMessage, isWarning);
502: } else {
503: return new CompilerError(new File(fileName), errMessage,
504: isWarning);
505: }
506: }
507:
508: /**
509: * Attempts to get the file from the given document.
510: * If the file has moved, we use the given FileSaveSelector to let the user save it
511: * in a new location.
512: *
513: * @param doc OpenDefinitionsDocument from which to get the file
514: * @param saver FileSaveSelector to allow the user to save the file if it has moved.
515: *
516: * @throws IllegalStateException if the doc has no file (hasn't been saved)
517: * @throws IOException if the file can't be saved after it was moved
518: */
519: private File _getFileFromDocument(OpenDefinitionsDocument doc,
520: FileSaveSelector saver) throws IOException {
521: try {
522: // This call will abort the iteration if there is no file, unless we can recover (like for a FileMovedException).
523: return doc.getFile();
524: } catch (FileMovedException fme) {
525: // The file has moved - prompt the user to recover.
526: // XXX: This is probably not thread safe!
527: if (saver.shouldSaveAfterFileMoved(doc, fme.getFile())) {
528: try {
529: doc.saveFileAs(saver);
530: return doc.getFile();
531: } catch (FileMovedException fme2) {
532: // If the user is this intent on shooting themselves in the foot,
533: // get out of the way.
534: fme2.printStackTrace();
535: throw new IOException("Could not find file: "
536: + fme2);
537: }
538: } else {
539: throw new IllegalStateException(
540: "No file exists for this document.");
541: }
542: }
543: }
544:
545: }
|