/* *******************************************************************
* Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Common Public License v1.0
* which accompanies this distribution and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* PARC initial implementation
* ******************************************************************/
package org.aspectj.tools.ajc;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.List;
import java.util.Date;
import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.ICommand;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.Message;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.bridge.ReflectionFactory;
import org.aspectj.bridge.Version;
import org.aspectj.bridge.context.CompilationAndWeavingContext;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
/**
* Programmatic and command-line interface to AspectJ compiler.
* The compiler is an ICommand obtained by reflection.
* Not thread-safe.
* By default, messages are printed as they are emitted;
* info messages go to the output stream, and
* warnings and errors go to the error stream.
* <p>
* Clients can handle all messages by registering a holder:
* <pre>Main main = new Main();
* IMessageHolder holder = new MessageHandler();
* main.setHolder(holder);</pre>
* Clients can get control after each command completes
* by installing a Runnable:
* <pre>main.setCompletionRunner(new Runnable() {..});</pre>
*
*/
public class Main {
/** Header used when rendering exceptions for users */
public static final String THROWN_PREFIX
= "Exception thrown from AspectJ "+ Version.text + LangUtil.EOL
+ ""+ LangUtil.EOL
+ "This might be logged as a bug already -- find current bugs at" + LangUtil.EOL
+ " http://bugs.eclipse.org/bugs/buglist.cgi?product=AspectJ&component=Compiler" + LangUtil.EOL
+ "" + LangUtil.EOL
+ "Bugs for exceptions thrown have titles File:line from the top stack, " + LangUtil.EOL
+ "e.g., \"SomeFile.java:243\"" + LangUtil.EOL
+ "" + LangUtil.EOL
+ "If you don't find the exception below in a bug, please add a new bug" + LangUtil.EOL
+ "at http://bugs.eclipse.org/bugs/enter_bug.cgi?product=AspectJ" + LangUtil.EOL
+ "To make the bug a priority, please include a test program" + LangUtil.EOL
+ "that can reproduce this exception." + LangUtil.EOL;
private static final String OUT_OF_MEMORY_MSG
= "AspectJ " + Version.text + " ran out of memory during compilation:" + LangUtil.EOL + LangUtil.EOL
+ "Please increase the memory available to ajc by editing the ajc script " + LangUtil.EOL
+ "found in your AspectJ installation directory. The -Xmx parameter value" + LangUtil.EOL
+ "should be increased from 64M (default) to 128M or even 256M." + LangUtil.EOL + LangUtil.EOL
+ "See the AspectJ FAQ available from the documentation link" + LangUtil.EOL
+ "on the AspectJ home page at http://www.eclipse.org/aspectj";
/** @param args the String[] of command-line arguments */
public static void main(String[] args) throws IOException {
new Main().runMain(args, true);
}
/**
* Convenience method to run ajc and collect String lists of messages.
* This can be reflectively invoked with the
* List collecting parameters supplied by a parent class loader.
* The String messages take the same form as command-line messages.
* @param args the String[] args to pass to the compiler
* @param useSystemExit if true and errors, return System.exit(errs)
* @param fails the List sink, if any, for String failure (or worse) messages
* @param errors the List sink, if any, for String error messages
* @param warnings the List sink, if any, for String warning messages
* @param info the List sink, if any, for String info messages
* @return number of messages reported with level ERROR or above
* @throws any unchecked exceptions the compiler does
*/
public static int bareMain(
String[] args,
boolean useSystemExit,
List fails,
List errors,
List warnings,
List infos) {
Main main = new Main();
MessageHandler holder = new MessageHandler();
main.setHolder(holder);
try {
main.runMain(args, useSystemExit);
} finally {
readMessages(holder, IMessage.FAIL, true, fails);
readMessages(holder, IMessage.ERROR, false, errors);
readMessages(holder, IMessage.WARNING, false, warnings);
readMessages(holder, IMessage.INFO, false, infos);
}
return holder.numMessages(IMessage.ERROR, true);
}
/** Read messages of a given kind into a List as String */
private static void readMessages(
IMessageHolder holder,
IMessage.Kind kind,
boolean orGreater,
List sink) {
if ((null == sink) || (null == holder)) {
return;
}
IMessage[] messages = holder.getMessages(kind, orGreater);
if (!LangUtil.isEmpty(messages)) {
for (int i = 0; i < messages.length; i++) {
sink.add(MessagePrinter.render(messages[i]));
}
}
}
/**
* @return String rendering throwable as compiler error for user/console,
* including information on how to report as a bug.
* @throws NullPointerException if thrown is null
*/
public static String renderExceptionForUser(Throwable thrown) {
String m = thrown.getMessage();
return THROWN_PREFIX
+ (null != m ? m + "\n": "")
+ "\n" + CompilationAndWeavingContext.getCurrentContext()
+ LangUtil.renderException(thrown, true);
}
private static String parmInArgs(String flag, String[] args) {
int loc = 1+(null == args ? -1 :Arrays.asList(args).indexOf(flag));
return ((0 == loc) || (args.length <= loc)?null:args[loc]);
}
private static boolean flagInArgs(String flag, String[] args) {
return ((null != args) && (Arrays.asList(args).indexOf(flag) != -1));
}
/** append nothing if numItems is 0,
* numItems + label + (numItems > 1? "s" : "") otherwise,
* prefixing with " " if sink has content
*/
private static void appendNLabel(StringBuffer sink, String label, int numItems) {
if (0 == numItems) {
return;
}
if (0 < sink.length()) {
sink.append(", ");
}
sink.append(numItems + " ");
if (!LangUtil.isEmpty(label)) {
sink.append(label);
}
if (1 < numItems) {
sink.append("s");
}
}
/** control iteration/continuation for command (compiler) */
protected CommandController controller;
/** ReflectionFactory identifier for command (compiler) */
protected String commandName;
/** client-set message sink */
private IMessageHolder clientHolder;
/** internally-set message sink */
protected final MessageHandler ourHandler;
private int lastFails;
private int lastErrors;
/** if not null, run this synchronously after each compile completes */
private Runnable completionRunner;
public Main() {
controller = new CommandController();
commandName = ReflectionFactory.ECLIPSE;
ourHandler = new MessageHandler(true);
}
public MessageHandler getMessageHandler() {
return ourHandler;
}
// for unit testing...
void setController(CommandController controller) {
this.controller = controller;
}
/**
* Run without throwing exceptions but optionally using System.exit(..).
* This sets up a message handler which emits messages immediately,
* so report(boolean, IMessageHandler) only reports total number
* of errors or warnings.
* @param args the String[] command line for the compiler
* @param useSystemExit if true, use System.exit(int) to complete
* unless one of the args is -noExit.
* and signal result (0 no exceptions/error, <0 exceptions, >0 compiler errors).
*/
public void runMain(String[] args, boolean useSystemExit) {
final boolean verbose = flagInArgs("-verbose", args);
IMessageHolder holder = clientHolder;
if (null == holder) {
holder = ourHandler;
if (verbose) {
ourHandler.setInterceptor(MessagePrinter.VERBOSE);
} else {
ourHandler.ignore(IMessage.INFO);
ourHandler.setInterceptor(MessagePrinter.TERSE);
}
}
// make sure we handle out of memory gracefully...
try {
// byte[] b = new byte[100000000]; for testing OoME only!
run(args, holder);
} catch (OutOfMemoryError outOfMemory) {
IMessage outOfMemoryMessage = new Message(OUT_OF_MEMORY_MSG,null,true);
holder.handleMessage(outOfMemoryMessage);
systemExit(holder); // we can't reasonably continue from this point.
}
boolean skipExit = false;
if (useSystemExit && !LangUtil.isEmpty(args)) { // sigh - pluck -noExit
for (int i = 0; i < args.length; i++) {
if ("-noExit".equals(args[i])) {
skipExit = true;
break;
}
}
}
if (useSystemExit && !skipExit) {
systemExit(holder);
}
}
/**
* Run without using System.exit(..), putting all messages in holder:
* <ul>
* <li>ERROR: compiler error</li>
* <li>WARNING: compiler warning</li>
* <li>FAIL: command error (bad arguments, exception thrown)</li>
* </ul>
* This handles incremental behavior:
* <ul>
* <li>If args include "-incremental", repeat for every input char
* until 'q' is entered.<li>
* <li>If args include "-incrementalTagFile {file}", repeat every time
* we detect that {file} modification time has changed. </li>
* <li>Either way, list files recompiled each time if args includes "-verbose".</li>
* <li>Exit when the commmand/compiler throws any Throwable.</li>
* </ul>
* When complete, this contains all the messages of the final
* run of the command and/or any FAIL messages produced in running
* the command, including any Throwable thrown by the command itself.
*
* @param args the String[] command line for the compiler
* @param holder the MessageHandler sink for messages.
*/
public void run(String[] args, IMessageHolder holder) {
PrintStream logStream = null;
FileOutputStream fos = null;
String logFileName = parmInArgs("-log", args);
if (null != logFileName){
File logFile = new File(logFileName);
try{
logFile.createNewFile();
fos = new FileOutputStream(logFileName, true);
logStream = new PrintStream(fos,true);
} catch(Exception e){
fail(holder, "Couldn't open log file: " + logFileName, e);
}
Date now = new Date();
logStream.println(now.toString());
if (flagInArgs("-verbose", args)) {
ourHandler.setInterceptor(new LogModeMessagePrinter(true,logStream));
} else {
ourHandler.ignore(IMessage.INFO);
ourHandler.setInterceptor(new LogModeMessagePrinter(false,logStream));
}
holder = ourHandler;
}
if (LangUtil.isEmpty(args)) {
args = new String[] { "-?" };
} else if (controller.running()) {
fail(holder, "already running with controller: " + controller, null);
return;
}
args = controller.init(args, holder);
if (0 < holder.numMessages(IMessage.ERROR, true)) {
return;
}
ICommand command = ReflectionFactory.makeCommand(commandName, holder);
if (0 < holder.numMessages(IMessage.ERROR, true)) {
return;
}
try {
outer:
while (true) {
boolean passed = command.runCommand(args, holder);
if (report(passed, holder) && controller.incremental()) {
while (controller.doRepeatCommand(command)) {
holder.clearMessages();
if (controller.buildFresh()) {
continue outer;
} else {
passed = command.repeatCommand(holder);
}
if (!report(passed, holder)) {
break;
}
}
}
break;
}
} catch (AbortException ae) {
if (ae.isSilent()) {
quit();
} else {
IMessage message = ae.getIMessage();
Throwable thrown = ae.getThrown();
if (null == thrown) { // toss AbortException wrapper
if (null != message) {
holder.handleMessage(message);
} else {
fail(holder, "abort without message", ae);
}
} else if (null == message) {
fail(holder, "aborted", thrown);
} else {
String mssg = MessageUtil.MESSAGE_MOST.renderToString(message);
fail(holder, mssg, thrown);
}
}
} catch (Throwable t) {
fail(holder, "unexpected exception", t);
} finally{
if (logStream != null){
logStream.close();
logStream = null;
}
if (fos != null){
try {
fos.close();
} catch (IOException e){
fail(holder, "unexpected exception", e);
}
fos = null;
}
}
}
/** call this to stop after the next iteration of incremental compile */
public void quit() {
controller.quit();
}
/**
* Set holder to be passed all messages.
* When holder is set, messages will not be printed by default.
* @param holder the IMessageHolder sink for all messages
* (use null to restore default behavior)
*/
public void setHolder(IMessageHolder holder) {
clientHolder = holder;
}
/**
* Install a Runnable to be invoked synchronously
* after each compile completes.
* @param runner the Runnable to invoke - null to disable
*/
public void setCompletionRunner(Runnable runner) {
this.completionRunner = runner;
}
/**
* Call System.exit(int) with values derived from the number
* of failures/aborts or errors in messages.
* @param messages the IMessageHolder to interrogate.
* @param messages
*/
protected void systemExit(IMessageHolder messages) {
int num = lastFails; // messages.numMessages(IMessage.FAIL, true);
if (0 < num) {
System.exit(-num);
}
num = lastErrors; // messages.numMessages(IMessage.ERROR, false);
if (0 < num) {
System.exit(num);
}
System.exit(0);
}
/** Messages to the user */
protected void outMessage(String message) { // XXX coordinate with MessagePrinter
System.out.print(message);
System.out.flush();
}
/**
* Report results from a (possibly-incremental) compile run.
* This delegates to any reportHandler or otherwise
* prints summary counts of errors/warnings to System.err (if any errors)
* or System.out (if only warnings).
* WARNING: this silently ignores other messages like FAIL,
* but clears the handler of all messages when returning true. XXX false
*
* This implementation ignores the pass parameter but
* clears the holder after reporting
* on the assumption messages were handled/printed already.
* (ignoring UnsupportedOperationException from holder.clearMessages()).
* @param pass true result of the command
* @param holder IMessageHolder with messages from the command
* @see reportCommandResults(IMessageHolder)
* @return false if the process should abort
*/
protected boolean report(boolean pass, IMessageHolder holder) {
lastFails = holder.numMessages(IMessage.FAIL, true);
boolean result = (0 == lastFails);
final Runnable runner = completionRunner;
if (null != runner) {
runner.run();
}
if (holder == ourHandler) {
lastErrors = holder.numMessages(IMessage.ERROR, false);
int warnings = holder.numMessages(IMessage.WARNING, false);
StringBuffer sb = new StringBuffer();
appendNLabel(sb, "fail|abort", lastFails);
appendNLabel(sb, "error", lastErrors);
appendNLabel(sb, "warning", warnings);
if (0 < sb.length()) {
PrintStream out = (0 < (lastErrors + lastFails)
? System.err
: System.out);
out.println(""); // XXX "wrote class file" messages no eol?
out.println(sb.toString());
}
}
return result;
}
/** convenience API to make fail messages (without MessageUtils's fail prefix) */
protected static void fail(IMessageHandler handler, String message, Throwable thrown) {
handler.handleMessage(new Message(message, IMessage.FAIL, thrown, null));
}
/**
* interceptor IMessageHandler to print as we go.
* This formats all messages to the user.
*/
public static class MessagePrinter implements IMessageHandler {
public static final IMessageHandler VERBOSE
= new MessagePrinter(true);
public static final IMessageHandler TERSE
= new MessagePrinter(false);
final boolean verbose;
protected MessagePrinter(boolean verbose) {
this.verbose = verbose;
}
/**
* Print errors and warnings to System.err,
* and optionally info to System.out,
* rendering message String only.
* @return false always
*/
public boolean handleMessage(IMessage message) {
if (null != message) {
PrintStream out = getStreamFor(message.getKind());
if (null != out) {
out.println(render(message));
}
}
return false;
}
/**
* Render message differently.
* If abort, then prefix stack trace with feedback request.
* If the actual message is empty, then use toString on the whole.
* Prefix message part with file:line;
* If it has context, suffix message with context.
* @param message the IMessage to render
* @return String rendering IMessage (never null)
*/
public static String render(IMessage message) {
// IMessage.Kind kind = message.getKind();
StringBuffer sb = new StringBuffer();
String text = message.getMessage();
if (text.equals(AbortException.NO_MESSAGE_TEXT)) {
text = null;
}
boolean toString = (LangUtil.isEmpty(text));
if (toString) {
text = message.toString();
}
ISourceLocation loc = message.getSourceLocation();
String context = null;
if (null != loc) {
File file = loc.getSourceFile();
if (null != file) {
String name = file.getName();
if (!toString || (-1 == text.indexOf(name))) {
sb.append(FileUtil.getBestPath(file));
if (loc.getLine() > 0) {
sb.append(":" + loc.getLine());
}
int col = loc.getColumn();
if (0 < col) {
sb.append(":" + col);
}
sb.append(" ");
}
}
context = loc.getContext();
}
// per Wes' suggestion on dev...
if (message.getKind() == IMessage.ERROR) {
sb.append("[error] ");
} else if (message.getKind() == IMessage.WARNING) {
sb.append("[warning] ");
}
sb.append(text);
if (null != context) {
sb.append(LangUtil.EOL);
sb.append(context);
}
String details = message.getDetails();
if (details != null) {
sb.append(LangUtil.EOL);
sb.append('\t');
sb.append(details);
}
Throwable thrown = message.getThrown();
if (null != thrown) {
sb.append(LangUtil.EOL);
sb.append(Main.renderExceptionForUser(thrown));
}
if (message.getExtraSourceLocations().isEmpty()) {
return sb.toString();
} else {
return MessageUtil.addExtraSourceLocations(message, sb.toString());
}
}
public boolean isIgnoring(IMessage.Kind kind) {
return (null != getStreamFor(kind));
}
/**
* No-op
* @see org.aspectj.bridge.IMessageHandler#isIgnoring(org.aspectj.bridge.IMessage.Kind)
* @param kind
*/
public void dontIgnore(IMessage.Kind kind) {
;
}
/** @return System.err for FAIL, ABORT, ERROR, and WARNING,
* System.out for INFO if -verbose and WEAVEINFO if -showWeaveInfo.
*/
protected PrintStream getStreamFor(IMessage.Kind kind) {
if (IMessage.WARNING.isSameOrLessThan(kind)) {
return System.err;
} else if (verbose && IMessage.INFO.equals(kind)) {
return System.out;
} else if (IMessage.WEAVEINFO.equals(kind)) {
return System.out;
} else {
return null;
}
}
}
public static class LogModeMessagePrinter extends MessagePrinter {
protected final PrintStream logStream;
public LogModeMessagePrinter(boolean verbose, PrintStream logStream) {
super(verbose);
this.logStream = logStream;
}
protected PrintStream getStreamFor(IMessage.Kind kind) {
if (IMessage.WARNING.isSameOrLessThan(kind)) {
return logStream;
} else if (verbose && IMessage.INFO.equals(kind)) {
return logStream;
} else if (IMessage.WEAVEINFO.equals(kind)) {
return logStream;
} else {
return null;
}
}
}
/** controller for repeatable command delays until input or file changed or removed */
public static class CommandController {
public static String TAG_FILE_OPTION = "-XincrementalFile";
public static String INCREMENTAL_OPTION = "-incremental";
/** maximum 10-minute delay between filesystem checks */
public static long MAX_DELAY = 1000 * 600;
/** default 5-second delay between filesystem checks */
public static long DEFAULT_DELAY = 1000 * 5;
/** @see init(String[]) */
private static String[][] OPTIONS = new String[][]
{ new String[] { INCREMENTAL_OPTION },
new String[] { TAG_FILE_OPTION, null } };
/** true between init(String[]) and doRepeatCommand() that returns false */
private boolean running;
/** true after quit() called */
private boolean quit;
/** true if incremental mode, waiting for input other than 'q' */
private boolean incremental;
/** true if incremental mode, waiting for file to change (repeat) or disappear (quit) */
private File tagFile;
/** last modification time for tagFile as of last command - 0 to start */
private long fileModTime;
/** delay between filesystem checks for tagFile modification time */
private long delay;
/** true just after user types 'r' for rebuild */
private boolean buildFresh;
public CommandController() {
delay = DEFAULT_DELAY;
}
/**
* @param argList read and strip incremental args from this
* @param sink IMessageHandler for error messages
* @return String[] remainder of args
*/
public String[] init(String[] args, IMessageHandler sink) {
running = true;
// String[] unused;
if (!LangUtil.isEmpty(args)) {
String[][] options = LangUtil.copyStrings(OPTIONS);
/*unused = */LangUtil.extractOptions(args, options);
incremental = (null != options[0][0]);
if (null != options[1][0]) {
File file = new File(options[1][1]);
if (!file.exists()) {
MessageUtil.abort(sink, "tag file does not exist: " + file);
} else {
tagFile = file;
fileModTime = tagFile.lastModified();
}
}
}
return args;
}
/** @return true if init(String[]) called but doRepeatCommand has not
* returned false */
public boolean running() {
return running;
}
/** @param delay milliseconds between filesystem checks */
public void setDelay(long delay) {
if ((delay > -1) && (delay < MAX_DELAY)) {
this.delay = delay;
}
}
/** @return true if INCREMENTAL_OPTION or TAG_FILE_OPTION was in args */
public boolean incremental() {
return (incremental || (null != tagFile));
}
/** @return true if INCREMENTAL_OPTION was in args */
public boolean commandLineIncremental() {
return incremental;
}
public void quit() {
if (!quit) {
quit = true;
}
}
/** @return true just after user typed 'r' */
boolean buildFresh() {
return buildFresh;
}
/** @return false if we should quit, true to do another command */
boolean doRepeatCommand(ICommand command) {
if (!running) {
return false;
}
boolean result = false;
if (quit) {
result = false;
} else if (incremental) {
try {
if (buildFresh) { // reset before input request
buildFresh = false;
}
System.out.println(" press enter to recompile, r to rebuild, q to quit: ");
System.out.flush();
// boolean doMore = false;
// seek for one q or a series of [\n\r]...
do {
int input = System.in.read();
if ('q' == input) {
break; // result = false;
} else if ('r' == input) {
buildFresh = true;
result = true;
} else if (('\n' == input) || ('\r' == input)) {
result = true;
} // else eat anything else
} while (!result);
System.in.skip(Integer.MAX_VALUE);
} catch (IOException e) { // XXX silence for error?
result = false;
}
} else if (null != tagFile) {
long curModTime;
while (true) {
if (!tagFile.exists()) {
result = false;
break;
} else if (fileModTime == (curModTime = tagFile.lastModified())) {
fileCheckDelay();
} else {
fileModTime = curModTime;
result = true;
break;
}
}
} // else, not incremental - false
if (!result && running) {
running = false;
}
return result;
}
/** delay between filesystem checks, returning if quit is set */
protected void fileCheckDelay() {
// final Thread thread = Thread.currentThread();
long targetTime = System.currentTimeMillis() + delay;
// long curTime;
while (targetTime > System.currentTimeMillis()) {
if (quit) {
return;
}
try { Thread.sleep(300); } // 1/3-second delta for quit check
catch (InterruptedException e) {}
}
}
}
}