| /* |
| * 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; |
| } |
| } |