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.compiler;
038:
039: import java.io.File;
040: import java.io.IOException;
041:
042: import java.util.List;
043: import java.util.ArrayList;
044: import java.util.Arrays;
045: import java.util.Collections;
046: import java.util.HashSet;
047: import java.util.LinkedList;
048: import java.util.Iterator;
049:
050: import edu.rice.cs.drjava.DrJava;
051: import edu.rice.cs.drjava.config.OptionConstants;
052:
053: import edu.rice.cs.drjava.model.GlobalModel;
054: import edu.rice.cs.drjava.model.OpenDefinitionsDocument;
055: import edu.rice.cs.drjava.model.definitions.InvalidPackageException;
056:
057: import edu.rice.cs.plt.io.IOUtil;
058: import edu.rice.cs.plt.iter.IterUtil;
059: import edu.rice.cs.util.swing.Utilities;
060: import edu.rice.cs.util.UnexpectedException;
061:
062: import edu.rice.cs.javalanglevels.*;
063: import edu.rice.cs.javalanglevels.parser.*;
064: import edu.rice.cs.javalanglevels.tree.*;
065:
066: /** Default implementation of the CompilerModel interface. This implementation is used for normal DrJava execution
067: * (as opposed to testing DrJava).
068: * @version $Id: DefaultCompilerModel.java 4255 2007-08-28 19:17:37Z mgricken $
069: */
070: public class DefaultCompilerModel implements CompilerModel {
071:
072: /** The available compilers */
073: private final List<CompilerInterface> _compilers;
074:
075: /** Current compiler -- one of _compilers, or a NoCompilerAvailable */
076: private CompilerInterface _active;
077:
078: /** Manages listeners to this model. */
079: private final CompilerEventNotifier _notifier = new CompilerEventNotifier();
080:
081: /** The global model to which this compiler model belongs. */
082: private final GlobalModel _model;
083:
084: /** The error model containing all current compiler errors. */
085: private CompilerErrorModel _compilerErrorModel;
086:
087: /** The working directory corresponding to the last compilation */
088: private File _workDir;
089:
090: /** The lock providing mutual exclustion between compilation and unit testing */
091: private Object _compilerLock = new Object();
092:
093: /** Main constructor.
094: * @param m the GlobalModel that is the source of documents for this CompilerModel
095: * @param compilers The compilers to use. The first will be made active; all are assumed
096: * to be available. An empty list is acceptable.
097: */
098: public DefaultCompilerModel(GlobalModel m,
099: Iterable<? extends CompilerInterface> compilers) {
100: _compilers = new ArrayList<CompilerInterface>();
101: for (CompilerInterface i : compilers) {
102: _compilers.add(i);
103: }
104: if (_compilers.size() > 0) {
105: _active = _compilers.get(0);
106: } else {
107: _active = NoCompilerAvailable.ONLY;
108: }
109:
110: _model = m;
111: _compilerErrorModel = new CompilerErrorModel(
112: new CompilerError[0], _model);
113: _workDir = _model.getWorkingDirectory();
114: }
115:
116: //--------------------------------- Locking -------------------------------//
117:
118: /** Returns the lock used to prevent simultaneous compilation and JUnit testing */
119: public Object getCompilerLock() {
120: return _compilerLock;
121: }
122:
123: //-------------------------- Listener Management --------------------------//
124:
125: /** Add a CompilerListener to the model.
126: * @param listener a listener that reacts to compiler events
127: *
128: * This operation is synchronized by the readers/writers protocol in EventNotifier<T>.
129: */
130: public void addListener(CompilerListener listener) {
131: _notifier.addListener(listener);
132: }
133:
134: /** Remove a CompilerListener from the model. If the listener is not currently
135: * listening to this model, this method has no effect.
136: * @param listener a listener that reacts to compiler events
137: *
138: * This operation is synchronized by the readers/writers protocol in EventNotifier<T>.
139: */
140: public void removeListener(CompilerListener listener) {
141: _notifier.removeListener(listener);
142: }
143:
144: /** Removes all CompilerListeners from this model. */
145: public void removeAllListeners() {
146: _notifier.removeAllListeners();
147: }
148:
149: //-------------------------------- Triggers --------------------------------//
150:
151: /** Compile all open documents.
152: *
153: * <p>Before compiling, all unsaved and untitled documents are saved, and compilation ends if the user cancels this
154: * step. The compilation classpath and sourcepath includes the build directory (if it exists), the source roots,
155: * the project "extra classpath" (if it exists), the global "extra classpath", and the current JVM's classpath
156: * (which includes drjava.jar, containing JUnit classes).</p>
157: *
158: * This method formerly only compiled documents which were out of sync with their class file, as a performance
159: * optimization. However, bug #634386 pointed out that unmodified files could depend on modified files, in which
160: * case this command would not recompile a file in some situations when it should. Since we value correctness over
161: * performance, we now always compile all open documents.</p>
162: *
163: * @throws IOException if a filesystem-related problem prevents compilation
164: */
165: public void compileAll() throws IOException {
166: if (_prepareForCompile()) {
167: _doCompile(_model.getOpenDefinitionsDocuments());
168: }
169: }
170:
171: /** Compiles all documents in the project source tree. Assumes DrJava currently contains an active project.
172: *
173: * <p>Before compiling, all unsaved and untitled documents are saved, and compilation ends if the user cancels this
174: * step. The compilation classpath and sourcepath includes the build directory (if it exists), the source roots,
175: * the project "extra classpath" (if it exists), the global "extra classpath", and the current JVM's classpath
176: * (which includes drjava.jar, containing JUnit classes).</p>
177: *
178: * This method formerly only compiled documents which were out of sync with their class file, as a performance
179: * optimization. However, bug #634386 pointed out that unmodified files could depend on modified files, in which
180: * case this command would not recompile a file in some situations when it should. Since we value correctness over
181: * performance, we now always compile all open documents.</p>
182: *
183: * @throws IOException if a filesystem-related problem prevents compilation
184: */
185: public void compileProject() throws IOException {
186: if (!_model.isProjectActive())
187: throw new UnexpectedException(
188: "compileProject invoked when DrJava is not in project mode");
189:
190: if (_prepareForCompile()) {
191: _doCompile(_model.getProjectDocuments());
192: }
193: }
194:
195: /** Compiles all of the given files.
196: *
197: * <p>Before compiling, all unsaved and untitled documents are saved, and compilation ends if the user cancels this
198: * step. The compilation classpath and sourcepath includes the build directory (if it exists), the source roots,
199: * the project "extra classpath" (if it exists), the global "extra classpath", and the current JVM's classpath
200: * (which includes drjava.jar, containing JUnit classes).</p>
201: *
202: * This method formerly only compiled documents which were out of sync with their class file, as a performance
203: * optimization. However, bug #634386 pointed out that unmodified files could depend on modified files, in which
204: * case this command would not recompile a file in some situations when it should. Since we value correctness over
205: * performance, we now always compile all open documents.</p>
206: *
207: * @throws IOException if a filesystem-related problem prevents compilation
208: */
209: public void compile(List<OpenDefinitionsDocument> defDocs)
210: throws IOException {
211: if (_prepareForCompile()) {
212: _doCompile(defDocs);
213: }
214: }
215:
216: /** Compiles the given file.
217: *
218: * <p>Before compiling, all unsaved and untitled documents are saved, and compilation ends if the user cancels this
219: * step. The compilation classpath and sourcepath includes the build directory (if it exists), the source roots,
220: * the project "extra classpath" (if it exists), the global "extra classpath", and the current JVM's classpath
221: * (which includes drjava.jar, containing JUnit classes).</p>
222: *
223: * This method formerly only compiled documents which were out of sync with their class file, as a performance
224: * optimization. However, bug #634386 pointed out that unmodified files could depend on modified files, in which
225: * case this command would not recompile a file in some situations when it should. Since we value correctness over
226: * performance, we now always compile all open documents.</p>
227: *
228: * @throws IOException if a filesystem-related problem prevents compilation
229: */
230: public void compile(OpenDefinitionsDocument doc) throws IOException {
231: if (_prepareForCompile()) {
232: _doCompile(Arrays.asList(doc));
233: }
234: }
235:
236: /** Check that there are no unsaved or untitled files currently open.
237: * @return @code{true} iff compilation should continue
238: */
239: private boolean _prepareForCompile() {
240: if (_model.hasModifiedDocuments())
241: _notifier.saveBeforeCompile();
242: // If user cancelled save, abort compilation
243: return !_model.hasModifiedDocuments();
244: }
245:
246: /** Compile the given documents. */
247: private void _doCompile(List<OpenDefinitionsDocument> docs)
248: throws IOException {
249: ArrayList<File> filesToCompile = new ArrayList<File>();
250: ArrayList<File> excludedFiles = new ArrayList<File>();
251: ArrayList<CompilerError> packageErrors = new ArrayList<CompilerError>();
252: for (OpenDefinitionsDocument doc : docs) {
253: if (doc.isSourceFile()) {
254: File f = doc.getFile();
255: // Check for null in case the file is untitled (not sure this is the correct check)
256: if (f != null) {
257: filesToCompile.add(f);
258: }
259: doc.setCachedClassFile(null); // clear cached class file
260:
261: try {
262: doc.getSourceRoot();
263: } catch (InvalidPackageException e) {
264: packageErrors.add(new CompilerError(f, e
265: .getMessage(), false));
266: }
267: } else
268: excludedFiles.add(doc.getFile());
269: }
270:
271: _notifier.compileStarted();
272: try {
273: if (!packageErrors.isEmpty()) {
274: _distributeErrors(packageErrors);
275: } else {
276: try {
277: File buildDir = _model.getBuildDirectory();
278: if ((buildDir != null) && !buildDir.exists()
279: && !buildDir.mkdirs()) {
280: throw new IOException(
281: "Could not create build directory: "
282: + buildDir);
283: }
284:
285: File workDir = _model.getWorkingDirectory();
286: if ((workDir != null) && !workDir.exists()
287: && !workDir.mkdirs()) {
288: throw new IOException(
289: "Could not create working directory: "
290: + workDir);
291: }
292:
293: _compileFiles(filesToCompile, buildDir);
294: } catch (Throwable t) {
295: CompilerError err = new CompilerError(t.toString(),
296: false);
297: _distributeErrors(Arrays.asList(err));
298: }
299: }
300: } finally {
301: _notifier.compileEnded(_model.getWorkingDirectory(),
302: excludedFiles);
303: }
304: }
305:
306: //-------------------------------- Helpers --------------------------------//
307:
308: /** Converts JExprParseExceptions thrown by the JExprParser in language levels to CompilerErrors. */
309: private LinkedList<CompilerError> _parseExceptions2CompilerErrors(
310: LinkedList<JExprParseException> pes) {
311: final LinkedList<CompilerError> errors = new LinkedList<CompilerError>();
312: Iterator<JExprParseException> iter = pes.iterator();
313: while (iter.hasNext()) {
314: JExprParseException pe = iter.next();
315: errors.addLast(new CompilerError(pe.getFile(),
316: pe.currentToken.beginLine - 1,
317: pe.currentToken.beginColumn - 1, pe.getMessage(),
318: false));
319: }
320: return errors;
321: }
322:
323: /** Converts errors thrown by the language level visitors to CompilerErrors. */
324: private LinkedList<CompilerError> _visitorErrors2CompilerErrors(
325: LinkedList<Pair<String, JExpressionIF>> visitorErrors) {
326: final LinkedList<CompilerError> errors = new LinkedList<CompilerError>();
327: Iterator<Pair<String, JExpressionIF>> iter = visitorErrors
328: .iterator();
329: while (iter.hasNext()) {
330: Pair<String, JExpressionIF> pair = iter.next();
331: String message = pair.getFirst();
332: // System.out.println("Got error message: " + message);
333: JExpressionIF jexpr = pair.getSecond();
334:
335: SourceInfo si;
336: if (jexpr == null)
337: si = JExprParser.NO_SOURCE_INFO;
338: else
339: si = pair.getSecond().getSourceInfo();
340:
341: errors.addLast(new CompilerError(si.getFile(), si
342: .getStartLine() - 1, si.getStartColumn() - 1,
343: message, false));
344: }
345: return errors;
346: }
347:
348: /** Compile the given files and update the model with any errors that result. Does not notify listeners.
349: * All public compile methods delegate to this one so this method is the only one that uses synchronization to
350: * prevent compiling and unit testing at the same time.
351: *
352: * @param files The files to be compiled
353: * @param buildDir The output directory for all the .class files; @code{null} means output to the same
354: * directory as the source file
355: *
356: */
357: private void _compileFiles(List<? extends File> files, File buildDir)
358: throws IOException {
359: if (!files.isEmpty()) {
360: /* Canonicalize buildDir */
361: if (buildDir != null)
362: buildDir = IOUtil.attemptCanonicalFile(buildDir);
363:
364: List<File> classPath = IterUtil.asList(_model
365: .getClassPath());
366:
367: // Temporary hack to allow a boot class path to be specified
368: List<File> bootClassPath = null;
369: String bootProp = System
370: .getProperty("drjava.bootclasspath");
371: if (bootProp != null) {
372: bootClassPath = IterUtil.asList(IOUtil
373: .parsePath(bootProp));
374: }
375:
376: final LinkedList<CompilerError> errors = new LinkedList<CompilerError>();
377:
378: List<? extends File> preprocessedFiles = _compileLanguageLevelsFiles(
379: files, errors);
380:
381: if (errors.isEmpty()) {
382: CompilerInterface compiler = getActiveCompiler();
383:
384: synchronized (_compilerLock) {
385: if (preprocessedFiles == null) {
386: errors.addAll(compiler.compile(files,
387: classPath, null, buildDir,
388: bootClassPath, null, true));
389: } else {
390: /** If compiling a language level file, do not show warnings, as these are not caught by the language level parser */
391: errors.addAll(compiler.compile(
392: preprocessedFiles, classPath, null,
393: buildDir, bootClassPath, null, false));
394: }
395: }
396: }
397: _distributeErrors(errors);
398: } else {
399: // TODO: Is this necessary?
400: _distributeErrors(Collections.<CompilerError> emptyList());
401: }
402: }
403:
404: /** Compiles the language levels files in the list. Adds any errors to the given error list.
405: * @return An updated list for compilation containing no Language Levels files, or @code{null}
406: * if there were no Language Levels files to process.
407: */
408: private List<? extends File> _compileLanguageLevelsFiles(
409: List<? extends File> files, List<CompilerError> errors) {
410: // TODO: The classpath (and sourcepath, bootclasspath) should be an argument passed to Language Levels.
411: LanguageLevelConverter llc = new LanguageLevelConverter(
412: getActiveCompiler().version());
413: Pair<LinkedList<JExprParseException>, LinkedList<Pair<String, JExpressionIF>>> llErrors = llc
414: .convert(files.toArray(new File[0]));
415:
416: /* Rename any .dj0 files in files to be .java files, so the correct thing is compiled. The hashset is used to
417: * make sure we never send in duplicate files. This can happen if the java file was sent in along with the
418: * corresponding .dj* file. The dj* file is renamed to a .java file and thus we have two of the same file in
419: * the list. By adding the renamed file to the hashset, the hashset efficiently removes duplicates.
420: */
421: HashSet<File> javaFileSet = new HashSet<File>();
422: boolean containsLanguageLevels = false;
423: for (File f : files) {
424: File canonicalFile = IOUtil.attemptCanonicalFile(f);
425: String fileName = canonicalFile.getPath();
426: int lastIndex = fileName.lastIndexOf(".dj");
427: if (lastIndex != -1) {
428: containsLanguageLevels = true;
429: javaFileSet.add(new File(fileName.substring(0,
430: lastIndex)
431: + ".java"));
432: } else {
433: javaFileSet.add(canonicalFile);
434: }
435: }
436: files = new LinkedList<File>(javaFileSet);
437:
438: errors.addAll(_parseExceptions2CompilerErrors(llErrors
439: .getFirst()));
440: errors.addAll(_visitorErrors2CompilerErrors(llErrors
441: .getSecond()));
442: if (containsLanguageLevels) {
443: return files;
444: } else {
445: return null;
446: }
447: }
448:
449: /** Sorts the given array of CompilerErrors and divides it into groups based on the file, giving each group to the
450: * appropriate OpenDefinitionsDocument, opening files if necessary. Called immediately after compilations finishes.
451: */
452: private void _distributeErrors(List<? extends CompilerError> errors)
453: throws IOException {
454: // resetCompilerErrors(); // Why is this done?
455: _compilerErrorModel = new CompilerErrorModel(errors
456: .toArray(new CompilerError[0]), _model);
457: _model.setNumCompErrors(_compilerErrorModel.getNumCompErrors()); // cache number of compiler errors in global model
458: }
459:
460: //----------------------------- Error Results -----------------------------//
461:
462: /** Gets the CompilerErrorModel representing the last compile. */
463: public CompilerErrorModel getCompilerErrorModel() {
464: return _compilerErrorModel;
465: }
466:
467: /** Gets the total number of errors in this compiler model. */
468: public int getNumErrors() {
469: return getCompilerErrorModel().getNumErrors();
470: }
471:
472: /** Gets the total number of current compiler errors. */
473: public int getNumCompErrors() {
474: return getCompilerErrorModel().getNumCompErrors();
475: }
476:
477: /** Gets the total number of current warnings. */
478: public int getNumWarnings() {
479: return getCompilerErrorModel().getNumWarnings();
480: }
481:
482: /** Resets the compiler error state to have no errors. */
483: public void resetCompilerErrors() {
484: // TODO: see if we can get by without this function
485: _compilerErrorModel = new CompilerErrorModel(
486: new CompilerError[0], _model);
487: }
488:
489: //-------------------------- Compiler Management --------------------------//
490:
491: /**
492: * Returns all registered compilers that are actually available. If there are none,
493: * the result is {@link NoCompilerAvailable#ONLY}.
494: */
495: public Iterable<CompilerInterface> getAvailableCompilers() {
496: if (_compilers.isEmpty()) {
497: return IterUtil.singleton(NoCompilerAvailable.ONLY);
498: } else {
499: return IterUtil.snapshot(_compilers);
500: }
501: }
502:
503: /**
504: * Gets the compiler that is the "active" compiler.
505: *
506: * @see #setActiveCompiler
507: */
508: public CompilerInterface getActiveCompiler() {
509: return _active;
510: }
511:
512: /**
513: * Sets which compiler is the "active" compiler.
514: *
515: * @param compiler Compiler to set active.
516: * @throws IllegalArgumentException If the compiler is not in the list of available compilers
517: *
518: * @see #getActiveCompiler
519: */
520: public void setActiveCompiler(CompilerInterface compiler) {
521: if (_compilers.isEmpty()
522: && compiler.equals(NoCompilerAvailable.ONLY)) {
523: // _active should be set correctly already
524: } else if (_compilers.contains(compiler)) {
525: _active = compiler;
526: } else {
527: throw new IllegalArgumentException(
528: "Compiler is not in the list of available compilers: "
529: + compiler);
530: }
531: }
532:
533: /** Add a compiler to the active list */
534: public void addCompiler(CompilerInterface compiler) {
535: if (_compilers.isEmpty()) {
536: _active = compiler;
537: }
538: _compilers.add(compiler);
539: }
540:
541: }
|