blob: fc268069f72d0542e0d24e1cb7dffb6e36a09836 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.java.project.ui;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.module.spi.AntEvent;
import org.apache.tools.ant.module.spi.AntLogger;
import org.apache.tools.ant.module.spi.AntSession;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.GlobalPathRegistry;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.IOColorPrint;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputListener;
import org.openide.windows.OutputWriter;
/**
* Ant logger which handles Java- and Java-project-specific UI.
* Specifically, handles stack traces hyperlinking and suppresses
* hyperlinking of nbproject/build-impl.xml files.
* @author Jesse Glick
* @see "#42525"
*/
@ServiceProvider(service=AntLogger.class, position=50)
public final class JavaAntLogger extends AntLogger {
public static final int LOGGER_MAX_LINE_LENGTH = Integer.getInteger("logger.max.line.length", 3000); //NOI18N
private static final String MODULE_INFO_CLZ = "module-info.class"; //NOI18N
static final class StackTraceParse {
final String line;
final String resource;
final int lineNumber;
final String prePart, midPart, endPart;
StackTraceParse(String line, String resource, int lineNumber, String prePart, String midPart, String endPart) {
this.line = line;
this.resource = resource;
this.lineNumber = lineNumber;
this.prePart = prePart;
this.midPart = midPart;
this.endPart = endPart;
}
public @Override String toString() {
return resource + ":" + lineNumber;
}
void hyperlink(AntSession session, AntEvent event, FileObject source,
int messageLevel, int sessionLevel, SessionData data) {
if (messageLevel <= sessionLevel && !event.isConsumed()) {
final OutputListener hyperlink = session.createStandardHyperlink(source.toURL(), guessExceptionMessage(data), lineNumber, -1, -1, -1);
event.consume();
InputOutput io = session.getIO();
if (IOColorPrint.isSupported(io)) {
try {
OutputWriter out = messageLevel <= AntEvent.LOG_WARN ? io.getErr() : /* #174781 1/2 */io.getOut();
boolean important = prePart.contains(/* #174781 2/2 */"at ") && /* e.g. InstalledFileLocatorImpl.findCaller */!prePart.contains("WARNING"); // NOI18N
out.print(prePart);
IOColorPrint.print(io, midPart, hyperlink, important, null);
out.println(endPart);
return;
} catch (IOException x) {
Exceptions.printStackTrace(x);
}
}
session.println(line, true, hyperlink);
}
}
}
/** Java identifier */
private static final String JIDENT = "[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*"; // NOI18N
// should be consistent with org.apache.tools.ant.module.bridge.impl.ForkedJavaOverride
// should be consistent with org.netbeans.modules.java.j2seembedded.project.RemoteJavaAntLogger
// would be nice to match org.netbeans.modules.hudson.impl.JavaHudsonLogger.STACK_TRACE, but would need to copy more
/**
* <ol>
* <li>non-hyperlinkable initial portion
* <li>package
* <li>class
* <li>hyperlinkable portion
* <li>filename
* <li>line number
* <li>non-hyperlinkable final portion
* </ol>
*/
private static final Pattern STACK_TRACE = Pattern.compile(
"(.*?((?:" + JIDENT + "[.])*)(" + JIDENT + ")[.](?:" + JIDENT + "|<init>|<clinit>)" + // NOI18N
"[(])(((?:"+JIDENT+"(?:\\."+JIDENT+")*/)?" + JIDENT + "[.]java):([0-9]+)|Unknown Source)([)].*)"); // NOI18N
static StackTraceParse/*|null*/ parseStackTraceLine(String line) {
if (line.length() >= LOGGER_MAX_LINE_LENGTH) { // too long message, probably coming from user, so do not check for stacktrace
return null;
}
Matcher m = STACK_TRACE.matcher(line);
if (m.matches()) {
// We have a stack trace.
String pkg = m.group(2);
String filename = m.group(5);
int lineNumber;
if (filename != null) {
final int index = filename.indexOf('/'); //NOI18N
if (index >= 0) {
filename = filename.substring(index+1);
if (filename.isEmpty()) {
filename = null;
}
}
}
if (filename == null) {
filename = m.group(3).replaceFirst("[$].+", "") + ".java"; // NOI18N
lineNumber = 1;
} else {
lineNumber = Integer.parseInt(m.group(6));
}
return new StackTraceParse(line, pkg.replace('.', '/') + filename, lineNumber, m.group(1), m.group(4), m.group(7));
} else {
return null;
}
}
/**
* Regexp matching the first line of a stack trace, with the exception message.
* Captured groups:
* <ol>
* <li>unqualified name of exception class plus possible message
* </ol>
*/
private static final Pattern EXCEPTION_MESSAGE = Pattern.compile(
// #42894: JRockit uses "Main Thread" not "main"
"(?:Exception in thread \"(?:main|Main Thread)\" )?(?:(?:" + JIDENT + "\\.)+)(" + JIDENT + "(?:: .+)?)"); // NOI18N
/**
* Regexp matching part of a Java task's invocation debug message
* that specifies the classpath.
* Hack to find the classpath an Ant task is using.
* Cf. Commandline.describeArguments, issue #28190.
* Captured groups:
* <ol>
* <li>the classpath
* </ol>
*/
private static final Pattern CLASSPATH_ARGS = Pattern.compile("\r?\n'-classpath'\r?\n'(.*)'\r?\n"); // NOI18N
private static final Pattern MODULEPATH_ARGS = Pattern.compile("\r?\n'--module-path'\r?\n'(.*)'\r?\n"); // NOI18N
private static final Pattern UPGRADE_MODULEPATH_ARGS = Pattern.compile("\r?\n'--upgrade-module-path'\r?\n'(.*)'\r?\n"); // NOI18N
/**
* Regexp matching part of a Java task's invocation debug message
* that specifies java executable.
* Hack to find JDK used for execution.
*/
private static final Pattern JAVA_EXECUTABLE = Pattern.compile("^Executing '(.*)' with arguments:$", Pattern.MULTILINE); // NOI18N
/**
* Ant task names we will pay attention to.
*/
private static final String[] TASKS_OF_INTEREST = {
// XXX should this really be restricted? what about stack traces printed during shutdown?
"java", // NOI18N
// #44328: unit tests run a different task:
"junit", // NOI18N
"testng", // NOI18N
// Nice to handle stack traces from e.g. NB's own build system too!
"exec", // NOI18N
// #63065: Mobility execution
"nb-run", //NOI18N
};
private static final int[] LEVELS_OF_INTEREST = {
AntEvent.LOG_VERBOSE, // for CLASSPATH_ARGS
AntEvent.LOG_INFO, // for some stack traces
AntEvent.LOG_WARN, // for most stack traces
AntEvent.LOG_ERR, // for some stack traces, incl. those redelivered from StandardLogger
};
/**
* Data stored in the session.
*/
private static final class SessionData {
public ClassPath platformSources = null;
public String classpath = null;
public String modulepath = null;
public String upgradeModulepath = null;
public volatile Collection<FileObject> searchSourceRoots = null;
public volatile String possibleExceptionText = null;
public volatile String lastExceptionMessage = null;
public SessionData() {}
public void setClasspath(String cp) {
classpath = cp;
searchSourceRoots = null;
}
public void setModulepath(@NullAllowed final String mp) {
modulepath = mp;
searchSourceRoots = null;
}
public void setUpgradeModulepath(@NullAllowed final String ump) {
upgradeModulepath = ump;
searchSourceRoots = null;
}
public void setPlatformSources(ClassPath platformSources) {
this.platformSources = platformSources;
searchSourceRoots = null;
}
}
/** Default constructor for lookup. */
public JavaAntLogger() {}
@Override
public boolean interestedInSession(AntSession session) {
return true;
}
@Override
public boolean interestedInAllScripts(AntSession session) {
return true;
}
@Override
public String[] interestedInTargets(AntSession session) {
return AntLogger.ALL_TARGETS;
}
@Override
public String[] interestedInTasks(AntSession session) {
return TASKS_OF_INTEREST;
}
@Override
public int[] interestedInLogLevels(AntSession session) {
// XXX could exclude those in [INFO..ERR] greater than session.verbosity
return LEVELS_OF_INTEREST;
}
private SessionData getSessionData(AntSession session) {
SessionData data = (SessionData) session.getCustomData(this);
if (data == null) {
data = new SessionData();
session.putCustomData(this, data);
}
return data;
}
@Override
public void messageLogged(AntEvent event) {
AntSession session = event.getSession();
int messageLevel = event.getLogLevel();
int sessionLevel = session.getVerbosity();
SessionData data = getSessionData(session);
String line = event.getMessage();
assert line != null;
StackTraceParse parse = parseStackTraceLine(line);
if (parse != null) {
// Check to see if the class is listed in our per-task sourcepath.
// XXX could also look for -Xbootclasspath etc., but probably less important
for (FileObject root : getCurrentSourceRootsForClasspath(data)) {
FileObject source = root.getFileObject(parse.resource);
if (source != null) {
// Got it!
parse.hyperlink(session, event, source, messageLevel, sessionLevel, data);
break;
}
}
// Also check global sourcepath (sources of open projects, and sources
// corresponding to compile or boot classpaths of open projects).
// Fallback in case a JAR file is copied to an unknown location, etc.
// In this case we can't be sure that this source file really matches
// the .class used in the stack trace, but it is a good guess.
if (!event.isConsumed()) {
FileObject source = GlobalPathRegistry.getDefault().findResource(parse.resource);
if (source != null) {
parse.hyperlink(session, event, source, messageLevel, sessionLevel, data);
} else if (messageLevel <= sessionLevel && "java".equals(event.getTaskName())) {
event.consume();
session.println(line, event.getLogLevel() <= AntEvent.LOG_WARN, null);
}
}
} else {
// Track the last line which was not a stack trace - probably the exception message.
data.lastExceptionMessage = null;
data.possibleExceptionText = line;
}
// Look for classpaths.
if (messageLevel == AntEvent.LOG_VERBOSE) {
if (line.length() >= LOGGER_MAX_LINE_LENGTH) { // too long message, probably coming from user, so do not care about it
return;
}
Matcher m2 = CLASSPATH_ARGS.matcher(line);
if (m2.find()) {
String cp = m2.group(1);
data.setClasspath(cp);
}
m2 = MODULEPATH_ARGS.matcher(line);
if (m2.find()) {
final String mp = m2.group(1);
data.setModulepath(mp);
}
m2 = UPGRADE_MODULEPATH_ARGS.matcher(line);
if (m2.find()) {
final String ump = m2.group(1);
data.setUpgradeModulepath(ump);
}
// XXX should also probably clear classpath when taskFinished called
m2 = JAVA_EXECUTABLE.matcher(line);
if (m2.find()) {
String executable = m2.group(1);
ClassPath platformSources = findPlatformSources(executable);
if (platformSources != null) {
data.setPlatformSources(platformSources);
}
}
}
}
private ClassPath findPlatformSources(String javaExecutable) {
for (JavaPlatform p : JavaPlatformManager.getDefault().getInstalledPlatforms()) {
FileObject fo = p.findTool("java"); // NOI18N
if (fo != null) {
File f = FileUtil.toFile(fo);
if (f.getAbsolutePath().startsWith(javaExecutable)) {
return p.getSourceFolders();
}
}
}
return null;
}
/**
* Finds source roots corresponding to the apparently active classpath
* (as reported by logging from Ant when it runs the Java launcher with -cp).
*/
private static Collection<FileObject> getCurrentSourceRootsForClasspath(SessionData data) {
if (data.classpath == null &&
data.modulepath == null &&
data.upgradeModulepath == null) {
return Collections.emptySet();
}
Collection<FileObject> result;
synchronized (data) {
result = data.searchSourceRoots;
}
if (result == null) {
result = new LinkedHashSet<>();
addPath(data.classpath, result, false);
addPath(data.modulepath, result, true);
addPath(data.upgradeModulepath, result, true);
if (data.platformSources != null) {
result.addAll(Arrays.asList(data.platformSources.getRoots()));
} else {
// no platform found. use default one:
JavaPlatform plat = JavaPlatform.getDefault();
// in unit tests the default platform may be null:
if (plat != null) {
result.addAll(Arrays.asList(plat.getSourceFolders().getRoots()));
}
}
result = Collections.unmodifiableCollection(result);
synchronized (data) {
data.searchSourceRoots = result;
}
}
return result;
}
private static void addPath(
@NullAllowed final String path,
@NonNull Collection<? super FileObject> collector,
final boolean modulepath) {
if (path != null) {
final StringTokenizer tok = new StringTokenizer(path, File.pathSeparator);
while (tok.hasMoreTokens()) {
final String binrootS = tok.nextToken();
final File f = FileUtil.normalizeFile(new File(binrootS));
final Collection<? extends File> toAdd = modulepath ?
collectModules(f) :
Collections.singleton(f);
toAdd.forEach((e) -> {
final URL binroot = FileUtil.urlForArchiveOrDir(f);
if (binroot != null) {
final FileObject[] someRoots = SourceForBinaryQuery.findSourceRoots(binroot).getRoots();
Collections.addAll(collector, someRoots);
}
});
}
}
}
@NonNull
private static Collection<? extends File> collectModules(@NonNull final File root) {
if (root.isDirectory()) {
if (new File(root,MODULE_INFO_CLZ).isFile()) {
return Collections.singleton(root);
} else {
final File[] children = root.listFiles((e) -> {
try {
return FileUtil.isArchiveFile(BaseUtilities.toURI(e).toURL());
} catch (MalformedURLException mue) {
return false;
}
});
return children == null ?
Collections.emptyList() :
Arrays.asList(children);
}
} else {
return Collections.singleton(root);
}
}
private static String guessExceptionMessage(SessionData data) {
final String pet = data.possibleExceptionText;
String lem = data.lastExceptionMessage;
if (pet != null && pet.length() < LOGGER_MAX_LINE_LENGTH) { // not too long message, so check for exception
if (lem == null) {
Matcher m = EXCEPTION_MESSAGE.matcher(pet);
if (m.matches()) {
data.lastExceptionMessage = lem = m.group(1);
} else {
data.possibleExceptionText = null;
}
}
return lem;
}
return null;
}
}