/* *******************************************************************
* Copyright (c) 2004 IBM Corporation
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v 2.0
* which accompanies this distribution and is available at
* https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
*
* Contributors:
* Adrian Colyer,
* ******************************************************************/
package org.aspectj.tools.ajc;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import org.aspectj.asm.AsmManager;
import org.aspectj.asm.IProgramElement;
import org.aspectj.asm.IRelationship;
import org.aspectj.asm.IRelationshipMap;
import org.aspectj.asm.internal.Relationship;
import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.ICommand;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessage.Kind;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.context.CompilationAndWeavingContext;
import org.aspectj.testing.util.TestUtil;
import org.aspectj.util.FileUtil;
import static java.io.File.pathSeparator;
import static java.io.File.separator;
import static org.aspectj.tools.ajc.AjcTestCase.*;
/**
* The Ajc class is intended for use as part of a unit-test suite, it drives the AspectJ compiler and lets you check the compilation
* results. Compilations run in a sandbox that is created in C:\temp\ajcSandbox or /tmp/ajcSandbox depending on your platform.
*
* The expected usage of Ajc is through the TestCase superclass, AjcTestCase, which provides helper methods that conveniently drive
* the base functions exposed by this class.
*
*
* @see org.aspectj.tools.ajc.AjcTestCase
*/
public class Ajc {
private static final String BUILD_OUTPUT_FOLDER = "target";
public static final String outputFolder(String module) {
return pathSeparator + ".." + separator + module + separator + BUILD_OUTPUT_FOLDER + separator + "classes";
}
public static final String outputFolders(String... modules) {
StringBuilder s = new StringBuilder();
for (String module: modules) {
s.append(pathSeparator + ".." + separator + module + separator + BUILD_OUTPUT_FOLDER + separator + "classes");
}
return s.toString();
}
// ALSO SEE ANTSPEC AND AJCTESTCASE
private static final String TESTER_PATH =
outputFolder("testing-client")
+ outputFolder("runtime")
+ outputFolder("bcel-builder")
+ pathSeparator + CLASSPATH_JUNIT
+ pathSeparator + CLASSPATH_ASM
+ pathSeparator + CLASSPATH_ASM_COMMONS
+ outputFolder("bridge")
+ outputFolder("loadtime")
+ outputFolder("weaver")
+ outputFolder("org.aspectj.matcher")
+ outputFolder("bridge");
private CompilationResult result;
private File sandbox;
private File baseDir;
private final Main main;
private String[] ajcArgs;
private int incrementalStage = 10;
private boolean shouldEmptySandbox = true;
private final AjcCommandController controller;
public static boolean verbose = System.getProperty("aspectj.tests.verbose", "true").equals("true");
/**
* Constructs a new Ajc instance, with a new AspectJ compiler inside.
*/
public Ajc() {
main = new Main();
controller = new AjcCommandController();
main.setController(controller);
}
/**
* By default, each call to compile
creates a new sandbox (C:\temp\ajcSandbox\ajtTestxxx.tmp, or
* /tmp/ajcSandbox/ajcTestxxx.tmp depending on your platform). To write a test that performs multiple (non-incremental)
* compiles, building on the results of previous compilations, set 'should empty sandbox' to false after the first compile,
* which will cause subsequent compiles in the test to use the same directory and contents.
*/
public void setShouldEmptySandbox(boolean empty) {
this.shouldEmptySandbox = empty;
}
/**
* Call the compiler with the given arguments (args are exactly the same as you would pass to ajc on the command-line). The
* results of the compile are returned in a CompilationResult
, which provides for easy testing of results.
*
* The compilation happens in a sandbox (C:\temp\ajcSandbox\ajTestxxx.tmp or /tmp/ajcSandbox/ajcTestxxx.tmp depending on
* platform). Compiler arguments are adapted to the sandbox as follows.
*
*
* For every file or directory listed in an argument (source file, or component of inpath, aspectpath, sourceroots,
* classpath,...), if the file is specified using an absolute path then it is left unchanged, but if the file is specified using
* a relative path, and a base directory (see setBaseDir) has been provided, then files/directories are copied from the base
* directory to the sandbox, and the compiler arguments adjusted to reflect their new location.
*
*
* For example, given a baseDir of "tests/pr12345" and a compile command: "ajc src/A.java src/B.java", the files in
*
*
* tests/pr12345/
* src/
* A.java
* B.java
*
*
* are copied to:
*
*
* ajcSandbox/ajcTestxxx.tmp/
* src/
* A.java
* B.java
*
*
* If no classpath is specified (no -classpath in the arguments) the classpath will be set to include the sandbox directory,
* testing-client/bin (for the Tester class), and runtime/bin (for the AspectJ runtime). If a classpath is specified,
* then any relative directories in it will be made relative to the sandbox, and the testing-client and runtime bin directories
* are also added.
*
*
* If no output directory is specified (no -d in the arguments), the output directory is set to the sandbox. If a directory is
* specified, and the path is relative, it will be made relative to the sandbox.
*
*
*
*
* @param args The compiler arguments.
* @return a CompilationResult object with all the messages produced by the compiler, a description of the ajc command that was
* issued, and the standard output and error of the compile (excluding messages which are provided separately)
* @throws IOException
* @see org.aspectj.tools.ajc.CompilationResult
*/
public CompilationResult compile(String[] args) throws IOException {
incrementalStage = 10;
return compile(args, false);
}
private CompilationResult compile(String[] args, boolean isIncremental) throws IOException {
result = null;
ajcArgs = args;
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream pout = new PrintStream(out);
ByteArrayOutputStream err = new ByteArrayOutputStream();
PrintStream perr = new PrintStream(err);
PrintStream systemOut = System.out;
PrintStream systemErr = System.err;
System.setOut(pout);
System.setErr(perr);
List fails = new ArrayList<>();
List errors = new ArrayList<>();
List warnings = new ArrayList<>();
List infos = new ArrayList<>();
List weaves = new ArrayList<>();
try {
if (!isIncremental && shouldEmptySandbox) {
sandbox = TestUtil.createEmptySandbox();
}
args = adjustToSandbox(args, !isIncremental);
MessageHandler holder = new MessageHandler();
holder.setInterceptor(new AbortInterceptor());
main.setHolder(holder);
if (incrementalStage == 10 && hasSpecifiedIncremental(args)) {
// important to sleep after preparing the sandbox on first incremental stage (see notes in pr90806)
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
if (isIncremental) {
controller.doIncremental(holder);
} else {
main.runMain(args, false);
}
addMessagesTo(infos, holder.getMessages(IMessage.INFO, false));
addMessagesTo(warnings, holder.getWarnings());
addMessagesTo(errors, holder.getErrors());
addMessagesTo(fails, holder.getMessages(IMessage.FAIL, true));
addMessagesTo(weaves, holder.getMessages(IMessage.WEAVEINFO, false));
String stdOut = out.toString();
String stdErr = err.toString();
result = new CompilationResult(args, stdOut, stdErr, infos, errors, warnings, fails, weaves);
} finally {
System.setOut(systemOut);
System.setErr(systemErr);
}
if (verbose) {
System.err.println(result.getStandardError());
System.out.println(result.getStandardOutput());
System.out.println(result);
}
return result;
}
private boolean hasSpecifiedIncremental(String[] args) {
if (args == null)
return false;
for (String arg : args) {
if (arg.equals("-incremental"))
return true;
}
return false;
}
/**
* After compiling for the first time with compile(), if the -incremental option was specified you can do as many subsequent
* incremental compiles as you like by calling this method.
*
* Throws an IllegalStateException if you try and call this method without first doing a compile that specified the -incremental
* option.
*
*
* @return A CompilationResult giving the results of the most recent increment.
* @throws IOException
*/
public CompilationResult doIncrementalCompile() throws IOException {
if ((ajcArgs == null) || !isIncremental(ajcArgs)) {
throw new IllegalStateException(
"Can't do incremental compile unless -incremental specified and first compile has taken place");
}
incrementalStage += 10;
return compile(ajcArgs, true);
}
/**
* Return the result of the last compile or incremental compile. This is the same as the return value from the compile() or
* doIncrementalCompile() methods.
*/
public CompilationResult getLastCompilationResult() {
return result;
}
/**
* Get the sandbox directory used for the compilation.
*/
public File getSandboxDirectory() {
if (sandbox == null) {
sandbox = TestUtil.createEmptySandbox();
}
return sandbox;
}
/**
* Set the base directory relative to which all relative paths specified in the arguments to a compile will be interpreted.
*/
public void setBaseDir(File dir) {
if ((dir != null) && !dir.isDirectory())
throw new IllegalArgumentException(dir.getPath() + " is not a directory: "+dir.getAbsolutePath());
baseDir = dir;
}
private void addMessagesTo(List aList, IMessage[] messages) {
Collections.addAll(aList, messages);
}
private boolean isIncremental(String[] args) {
for (String arg : args) {
if (arg.trim().equals("-incremental"))
return true;
}
return false;
}
/**
* Make every relative file name and dir be absolute under sandbox Add TESTER_PATH to classpath
*/
private String[] adjustToSandbox(String[] args, boolean doCopy) throws IOException {
String[] newArgs = new String[args.length];
boolean hasClasspath = false;
boolean hasOutdir = false;
for (int i = 0; i < args.length; i++) {
newArgs[i] = args[i];
if (FileUtil.hasSourceSuffix(args[i])) {
File f = new File(args[i]);
// newArgs[i] = new File(baseDir,args[i]).getAbsolutePath(); // might be quicker?
newArgs[i] = adjustFileOrDir(f, doCopy, false).getAbsolutePath();
} else if (args[i].endsWith(".xml") && !args[i].startsWith("-")) {
if (i > 0 && args[i - 1].equals("-outxmlfile")) {
// dont adjust it
} else {
File f = new File(args[i]);
// newArgs[i] = new File(baseDir,args[i]).getAbsolutePath(); // might be quicker?
newArgs[i] = adjustFileOrDir(f, doCopy, false).getAbsolutePath();
}
} else {
if ((args[i].equals("-aspectpath") || args[i].equals("-inpath") || args[i].equals("-injars")
|| args[i].equals("-outjar") || args[i].equals("-classpath") || args[i].equals("-sourceroots")
|| args[i].equals("-Xlintfile") || args[i].equals("-extdirs") || args[i].equals("-d"))
&& args.length > (i + 1))
{
newArgs[i] = args[i];
StringBuilder buff = new StringBuilder();
boolean copyThisTime = doCopy;
if (args[i].equals("-d")) {
copyThisTime = false;
hasOutdir = true;
}
boolean isOutjar = args[i].equals("-outjar");
StringTokenizer strTok = new StringTokenizer(args[++i], pathSeparator);
while (strTok.hasMoreTokens()) {
File f = new File(strTok.nextToken());
buff.append(adjustFileOrDir(f, copyThisTime, isOutjar).getAbsolutePath());
if (strTok.hasMoreTokens())
buff.append(pathSeparator);
}
newArgs[i] = buff.toString();
if (args[i - 1].equals("-classpath")) {
hasClasspath = true;
newArgs[i] = newArgs[i] + pathSeparator + TESTER_PATH + pathSeparator
+ getSandboxDirectory().getAbsolutePath();
}
} else {
// could be resource file
File f = new File(args[i]);
if (f.exists()) {
newArgs[i] = adjustFileOrDir(f, doCopy, false).getAbsolutePath();
}
}
}
}
if (!hasClasspath) {
String[] oldArgs = newArgs;
newArgs = new String[oldArgs.length + 2];
System.arraycopy(oldArgs, 0, newArgs, 0, oldArgs.length);
newArgs[oldArgs.length] = "-classpath";
newArgs[oldArgs.length + 1] = TESTER_PATH + pathSeparator + getSandboxDirectory().getAbsolutePath();
}
if (!hasOutdir) {
String[] oldArgs = newArgs;
newArgs = new String[oldArgs.length + 2];
System.arraycopy(oldArgs, 0, newArgs, 0, oldArgs.length);
newArgs[oldArgs.length] = "-d";
newArgs[oldArgs.length + 1] = getSandboxDirectory().getPath();
}
return newArgs;
}
private File adjustFileOrDir(File from, boolean doCopy, boolean ensureDirsExist) throws IOException {
File to = from;
File ret = from;
if (!from.isAbsolute()) {
ret = new File(sandbox, from.getPath());
File fromParent = from.getParentFile();
String relativeToPath = (fromParent != null) ? (fromParent.getPath() + separator) : "";
if (baseDir != null) {
from = new File(baseDir, from.getPath());
// if (ensureDirsExist) {
// File toMkdir = (ret.getPath().endsWith(".jar") || ret.getPath().endsWith(".zip"))?ret.getParentFile():ret;
// toMkdir.mkdirs();
// }
}
if (!from.exists())
return ret;
if (doCopy) {
// harness requires that any files with the same name, and a different extension,
// get copied too (e.g. .out, .err, .event files)
if (from.isFile()) {
final String prefix = from.getName().substring(0, from.getName().lastIndexOf('.'));
String[] toCopy = from.getParentFile().list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.indexOf('.') == -1)
return false;
String toMatch = name.substring(0, name.lastIndexOf('.'));
return (toMatch.equals(prefix));
}
});
for (String s : toCopy) {
String toPath = relativeToPath + s;
to = new File(sandbox, toPath);
FileUtil.copyFile(new File(from.getParentFile(), s), to);
}
} else {
FileUtil.copyFile(from, ret);
}
}
}
return ret;
}
public static void dumpAJDEStructureModel(AsmManager model, String prefix) {
dumpAJDEStructureModel(model, prefix, false);
}
public static void dumpAJDEStructureModel(AsmManager model, String prefix, boolean useHandles) {
System.out.println("======================================");//$NON-NLS-1$
System.out.println("start of AJDE structure model:" + prefix); //$NON-NLS-1$
IRelationshipMap asmRelMap = model.getRelationshipMap();
for (String sourceOfRelationship : asmRelMap.getEntries()) {
System.err.println("Examining source relationship handle: " + sourceOfRelationship);
List relationships = null;
if (useHandles) {
relationships = asmRelMap.get(sourceOfRelationship);
} else {
IProgramElement ipe = model.getHierarchy().findElementForHandle(sourceOfRelationship);
relationships = asmRelMap.get(ipe);
}
if (relationships != null) {
for (IRelationship relationship : relationships) {
Relationship rel = (Relationship) relationship;
List targets = rel.getTargets();
for (String t : targets) {
IProgramElement link = model.getHierarchy().findElementForHandle(t);
System.out.println(""); //$NON-NLS-1$
System.out.println(" sourceOfRelationship " + sourceOfRelationship); //$NON-NLS-1$
System.out.println(" relationship " + rel.getName()); //$NON-NLS-1$
System.out.println(" target " + link.getName()); //$NON-NLS-1$
}
}
}
}
System.out.println("End of AJDE structure model"); //$NON-NLS-1$
System.out.println("======================================");//$NON-NLS-1$
}
}
/*
* So that we can drive incremental compilation easily from a unit test.
*/
class AjcCommandController extends Main.CommandController {
private ICommand command;
/*
* (non-Javadoc)
*
* @see org.aspectj.tools.ajc.Main.CommandController#doRepeatCommand()
*/
@Override
boolean doRepeatCommand(ICommand command) {
this.command = command;
return false; // ensure that control returns to caller
}
/*
* (non-Javadoc)
*
* @see org.aspectj.tools.ajc.Main.CommandController#running()
*/
@Override
public boolean running() {
return false; // so that we can come back for more...
}
public void doIncremental(IMessageHandler handler) {
if (command == null)
throw new IllegalArgumentException("Can't repeat command until it has executed at least once!");
command.repeatCommand(handler);
}
}
class AbortInterceptor implements IMessageHandler {
@Override
public boolean handleMessage(IMessage message) throws AbortException {
if (message.getKind() == IMessage.ABORT) {
System.err.println("***** Abort Message Received ******");
System.err.println(CompilationAndWeavingContext.getCurrentContext());
System.err.println(message.getMessage());
if (message.getThrown() != null) {
System.err.println("caused by " + message.getThrown().toString());
}
} // allow message to accumulate...
return false;
}
@Override
public boolean isIgnoring(Kind kind) {
if (kind != IMessage.ABORT)
return true;
return false;
}
@Override
public void dontIgnore(Kind kind) {
}
@Override
public void ignore(Kind kind) {
}
}