blob: 4eea70b45181f72cf0b4ff6468977222a2c81bc7 [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.web.webkit.tooling.console;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.SystemColor;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.UIManager;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.modules.web.clientproject.api.WebClientProjectConstants;
import org.netbeans.modules.web.common.api.RemoteFileCache;
import org.netbeans.modules.web.common.api.ServerURLMapping;
import org.netbeans.modules.web.common.api.WebUtils;
import org.netbeans.modules.web.webkit.debugging.api.console.Console;
import org.netbeans.modules.web.webkit.debugging.api.console.ConsoleMessage;
import org.openide.cookies.LineCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.Line;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.windows.FoldHandle;
import org.openide.windows.IOColorPrint;
import org.openide.windows.IOColors;
import org.openide.windows.IOFolding;
import org.openide.windows.IOProvider;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputEvent;
import org.openide.windows.OutputListener;
import org.openide.windows.OutputWriter;
/**
*
*/
public class BrowserConsoleLogger implements Console.Listener {
private static final String LEVEL_ERROR = "error"; // NOI18N
private static final String LEVEL_WARNING = "warning"; // NOI18N
private static final String LEVEL_DEBUG = "debug"; // NOI18N
private static final String PROMPT = "> "; // NOI18N
private final Lookup projectContext;
private InputOutput io;
private Color colorStdBrighter;
/** The last logged message. */
private ConsoleMessage lastMessage;
private Console.InputCallback input;
private ConsoleReader reader;
private boolean isFoldingSupported;
//private Color colorErrBrighter;
private final AtomicBoolean shownOnError = new AtomicBoolean(false);
private final RequestProcessor rp = new RequestProcessor(BrowserConsoleLogger.class);
private static final Logger LOG = Logger.getLogger(BrowserConsoleLogger.class.getName());
public BrowserConsoleLogger(Lookup projectContext) {
this.projectContext = projectContext;
initIO();
}
@NbBundle.Messages({"BrowserConsoleLoggerTitle=Browser Log"})
private void initIO() {
io = IOProvider.getDefault().getIO(Bundle.BrowserConsoleLoggerTitle(), false);
setIOColors();
io.setInputVisible(true);
isFoldingSupported = IOFolding.isSupported(io);
io.getOut().print(PROMPT);
Reader r = io.getIn();
reader = new ConsoleReader(r);
rp.post(reader);
}
private void setIOColors() {
if (IOColors.isSupported(io) && IOColorPrint.isSupported(io)) {
Color colorStd = IOColors.getColor(io, IOColors.OutputType.OUTPUT);
//Color colorErr = IOColors.getColor(io, IOColors.OutputType.ERROR);
Color background = UIManager.getDefaults().getColor("nb.output.background"); // NOI18N
if (background == null) {
background = SystemColor.window;
}
colorStdBrighter = shiftTowards(colorStd, background);
//colorErrBrighter = shiftTowards(colorErr, background);
Color foreground = UIManager.getDefaults().getColor("nb.output.foreground"); // NOI18N
if (foreground == null) {
foreground = SystemColor.textText;
}
IOColors.setColor(io, IOColors.OutputType.INPUT, shiftTowards(foreground, Color.GREEN));
}
}
public void close() {
LOG.fine("close() closing IO");
reader.closing();
io.getErr().close();
io.getOut().close();
try {
io.getIn().close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
private static Color shiftTowards(Color c, Color b) {
return new Color((c.getRed() + b.getRed())/2, (c.getGreen() + b.getGreen())/2, (c.getBlue() + b.getBlue())/2);
}
@Override
public void messageAdded(ConsoleMessage message) {
try {
lastMessage = message;
logMessage(message);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
@Override
public void messagesCleared() {
LOG.fine("messagesCleared() resetting IO");
try {
io.getOut().reset();
} catch (IOException ex) {}
setIOColors();
io.setInputVisible(true);
// HACK: Wait for the processing of reset event
try {
EventQueue.invokeAndWait(new Runnable() {
@Override public void run() {}
});
} catch (InterruptedException ex) {
} catch (InvocationTargetException ex) {}
io.getOut().print(PROMPT);
}
@Override
public void messageRepeatCountUpdated(int count) {
LOG.log(Level.FINE, "messageRepeatCountUpdated({0})", count);
try {
logMessage(lastMessage);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
private static final SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss:SSS");
public static String getCurrentTime() {
return formatter.format(new Date(System.currentTimeMillis()));
}
private boolean shouldLogMessage(ConsoleMessage msg) {
if (msg == null) {
return false; // Issue 250389
}
String level = msg.getLevel();
String source = msg.getSource();
// Ignore CSS warnings
if (LEVEL_WARNING.equals(level) && "css".equals(source)) { // NOI18N
return false;
}
return true;
}
private void logMessage(ConsoleMessage msg) throws IOException {
if (!shouldLogMessage(msg)) {
return ;
}
io.getOut().print("\b\b");
Project project = projectContext.lookup(Project.class);
logMessage(msg, project);
io.getOut().print(PROMPT);
boolean isErr = LEVEL_ERROR.equals(msg.getLevel());
if (io.isClosed() || (isErr && !shownOnError.getAndSet(true))) {
io.select();
}
}
private void logMessage(ConsoleMessage msg, Project project) throws IOException {
String level = msg.getLevel();
boolean isErr = LEVEL_ERROR.equals(level);
String time = getCurrentTime();
String logInfo = createLogInfo(time, level, msg.getSource(), msg.getType());
OutputWriter ow = isErr ? io.getErr() : io.getOut();
String text = msg.getText();
if (text != null) { // Text is not marked as optional, but apparently, it is.
String lines[] = text.replace("\r", "").split("\n");
for (int i = 0; i < lines.length; i++) {
String singleMessageLine = lines[i];
if (colorStdBrighter == null && i == lines.length-1) {
singleMessageLine += logInfo;
}
Object res[] = tryToConvertLineToHyperlink(project, singleMessageLine);
MyListener l = null;
String newMessage1 = null;
String newMessage2 = null;
if (res != null) {
l = (MyListener)res[0];
newMessage1 = (String)res[1];
newMessage2 = (String)res[2];
}
if (l != null && l.isValidHyperlink()) {
if (colorStdBrighter != null && i == lines.length-1) {
newMessage2 += logInfo;
}
ow.print(newMessage1);
ow.println(newMessage2, l);
} else {
ow.print(singleMessageLine);
if (colorStdBrighter != null && i == lines.length-1) {
//if (isErr) {
// IOColorPrint.print(io, logInfo, colorErrBrighter);
//} else {
IOColorPrint.print(io, logInfo, colorStdBrighter);
//}
} else {
ow.println("");
}
}
}
}
boolean doPrintStackTrace = LEVEL_ERROR.equals(level) ||
LEVEL_DEBUG.equals(level);
StringBuilder sb;
boolean first = true;
if (doPrintStackTrace && msg.getStackTrace() != null) {
FoldHandle fold = null;
if (isFoldingSupported) {
fold = IOFolding.startFold(io, false);
}
for (ConsoleMessage.StackFrame sf : msg.getStackTrace()) {
String indent;
if (first) {
indent = " at ";
first = false;
} else {
indent = " at ";
}
ow.print(indent);
ow.print(sf.getFunctionName());
sb = new StringBuilder();
String urlStr = sf.getURLString();
urlStr = getProjectPath(project, urlStr);
sb.append(" (").append(urlStr).append(":").append(sf.getLine()).append(":").append(sf.getColumn()).append(")");
MyListener l = new MyListener(project, sf.getURLString(), sf.getLine(), sf.getColumn());
if (l.isValidHyperlink()) {
ow.println(sb.toString(), l);
} else {
ow.println(sb.toString());
}
}
if (fold != null) {
fold.finish();
}
}
if (first && msg.getURLString() != null && msg.getURLString().length() > 0) {
ow.print(" at ");
String url = msg.getURLString();
String file = getProjectPath(project, url);
sb = new StringBuilder(file);
int line = msg.getLine();
if (line != -1 && line != 0) {
sb.append(":");
sb.append(line);
}
MyListener l = new MyListener(project, url, line, -1);
if (l.isValidHyperlink()) {
ow.println(sb.toString(), l);
} else {
ow.println(sb.toString());
}
}
List<ConsoleMessage> subMessages = msg.getSubMessages();
if (!subMessages.isEmpty()) {
FoldHandle fold = null;
if (isFoldingSupported) {
fold = IOFolding.startFold(io, false);
}
for (ConsoleMessage cm : subMessages) {
logMessage(cm, project);
}
if (fold != null) {
fold.finish();
}
}
}
@NbBundle.Messages({"BrowserConsoleLoggerSessionClosed=Debugging session with browser was closed."})
void sessionWasClosed() {
io.getOut().print("\b\b");
io.getOut().println(Bundle.BrowserConsoleLoggerSessionClosed());
}
// XXX: exact this algorithm is also in
// javascript.jstestdriver/src/org/netbeans/modules/javascript/jstestdriver/JSTestDriverSupport.java
// keep them in sync
private Object[] tryToConvertLineToHyperlink(Project project, String line) {
// pattern is "at ...... (file:line:column)"
// file can be also http:// url
if (!line.endsWith(")")) {
return tryToConvertLineURLToHyperlink(project, line);
}
int start = line.lastIndexOf('(');
if (start == -1) {
return null;
}
int lineNumberEnd = line.lastIndexOf(':');
if (lineNumberEnd == -1) {
return null;
}
int fileEnd = line.lastIndexOf(':', lineNumberEnd-1);
if (fileEnd == -1) {
return null;
}
if (start >= fileEnd) {
return null;
}
int lineNumber = -1;
int columnNumber = -1;
try {
lineNumber = Integer.parseInt(line.substring(fileEnd+1, lineNumberEnd));
columnNumber = Integer.parseInt(line.substring(lineNumberEnd+1, line.length()-1));
} catch (NumberFormatException e) {
//ignore
}
if (columnNumber != -1 && lineNumber == -1) {
// perhaps stack trace had only line number:
lineNumber = columnNumber;
}
if (lineNumber == -1) {
return null;
}
String file = line.substring(start+1, fileEnd);
if (file.length() == 0) {
return null;
}
String s1 = line.substring(0, start);
String s2 = "(" + // NOI18N
getProjectPath(project, file) +
line.substring(fileEnd, line.length());
MyListener l = new MyListener(project, file, lineNumber, columnNumber);
return new Object[]{l,s1,s2};
}
private Object[] tryToConvertLineURLToHyperlink(Project project, String line) {
int u1 = line.indexOf("http://"); // NOI18N
if (u1 < 0) {
u1 = line.indexOf("https://"); // NOI18N
}
if (u1 < 0) {
return null;
}
int ue = line.indexOf(' ', u1);
if (ue < 0) {
ue = line.length();
}
int col2 = line.lastIndexOf(':', ue);
if (col2 < 0) {
return null;
}
int col1 = line.lastIndexOf(':', col2 - 1);
if (col1 < 0) {
return null;
}
int lineNumber = -1;
int columnNumber = -1;
try {
lineNumber = Integer.parseInt(line.substring(col1+1, col2));
columnNumber = Integer.parseInt(line.substring(col2+1, ue));
} catch (NumberFormatException e) {
//ignore
}
if (columnNumber != -1 && lineNumber == -1) {
// perhaps stack trace had only line number:
lineNumber = columnNumber;
}
if (lineNumber == -1) {
return null;
}
String file = line.substring(u1, col1);
if (file.length() == 0) {
return null;
}
String s1 = line.substring(0, u1);
String s2 = line.substring(u1, line.length());
MyListener l = new MyListener(project, file, lineNumber, columnNumber);
return new Object[]{l,s1,s2};
}
private static final String LOG_IGNORED = "log"; // NOI18N
private static final String CONSOLE_API = "console-api"; // NOI18N
private static final String TIME_SEPARATOR = " | "; // NOI18N
private static String createLogInfo(String time, String level, String source, String type) {
//String logInfo = " ("+time+" | "+level+","+msg.getSource()+","+msg.getType()+")\n";
StringBuilder logInfoBuilder = new StringBuilder(" (");
logInfoBuilder.append(time);
boolean separator = false;
if (!LOG_IGNORED.equals(level) && !level.isEmpty()) {
separator = true;
logInfoBuilder.append(TIME_SEPARATOR);
logInfoBuilder.append(level);
}
if (!CONSOLE_API.equals(source) && !source.isEmpty()) {
if (separator) {
logInfoBuilder.append(", ");
} else {
logInfoBuilder.append(TIME_SEPARATOR);
}
logInfoBuilder.append(source);
}
if (!LOG_IGNORED.equals(type)) {
if (separator) {
logInfoBuilder.append(", ");
} else {
logInfoBuilder.append(TIME_SEPARATOR);
}
logInfoBuilder.append(type);
}
logInfoBuilder.append(")\n");
return logInfoBuilder.toString();
}
/**
* Try to find a more readable project-relative path.<p>
* E.g.: "http://localhost:89/SimpleLiveHTMLTest/js/app.js:8:9"
* is turned into: "js/app.js:8:9"
* @param urlStr The URL
* @return a project-relative path, or the original URL.
*/
public static String getProjectPath(Project project, String urlStr) {
try {
URL url = new URL(urlStr);
if (project != null) {
FileObject fo = ServerURLMapping.fromServer(project, url);
if (fo != null) {
String relPath = FileUtil.getRelativePath(project.getProjectDirectory(), fo);
if (relPath != null) {
urlStr = relPath;
}
}
}
} catch (MalformedURLException murl) {}
return urlStr;
}
void setInput(Console.InputCallback input) {
this.input = input;
}
public static class MyListener implements OutputListener {
private final String url;
private final int line;
private final int column;
private final Project project;
public MyListener(Project project, String url, int line, int column) {
this.url = url;
this.line = line;
this.column = column;
this.project = project;
}
@Override
public void outputLineSelected(OutputEvent ev) {
}
@Override
public void outputLineAction(OutputEvent ev) {
Line l = getLine();
if (l != null) {
l.show(Line.ShowOpenType.OPEN,
Line.ShowVisibilityType.FOCUS, column != -1 ? column -1 : -1);
}
}
private Line getLine() {
return BrowserConsoleLogger.getLine(project, url, line-1);
}
@Override
public void outputLineCleared(OutputEvent ev) {
}
public boolean isValidHyperlink() {
return getLine() != null;
}
}
private static Line getLine(Project project, final String filePath, final int lineNumber) {
if (filePath == null || lineNumber < 0) {
return null;
}
FileObject fileObject = null;
try {
if (filePath.startsWith("http:") || filePath.startsWith("https:")) { // NOI18N
URL url;
try {
url = new URI(filePath).toURL();
} catch (URISyntaxException usex) {
// Issue 231564
url = new URL(filePath);
}
if (project != null) {
fileObject = ServerURLMapping.fromServer(project, url);
}
if (fileObject == null) {
fileObject = RemoteFileCache.getRemoteFile(url);
}
} else {
File file;
if (filePath.startsWith("file:/")) { // NOI18N
URI uri;
try {
// Issue 255536
uri = new URI(filePath);
} catch (URISyntaxException usex) {
uri = null;
}
if (uri == null || uri.getQuery() != null || uri.getFragment() != null) {
// Remove the query and/or fragment
uri = WebUtils.stringToUrl(WebUtils.urlToString(new URL(filePath), true)).toURI();
}
file = Utilities.toFile(uri);
} else {
file = new File(filePath);
if (!file.isAbsolute() && project != null) {
fileObject = findFileInProjecSources(project, filePath);
}
}
if (fileObject == null) {
fileObject = FileUtil.toFileObject(FileUtil.normalizeFile(file));
}
}
} catch (IOException | URISyntaxException ex) {
Exceptions.printStackTrace(ex);
}
if (fileObject == null) {
LOG.log(Level.FINE, "Cannot resolve \"{0}\"", filePath);
return null;
}
LineCookie lineCookie = getLineCookie(fileObject);
if (lineCookie == null) {
LOG.log(Level.INFO, "No line cookie for \"{0}\"", fileObject);
return null;
}
try {
return lineCookie.getLineSet().getCurrent(lineNumber);
} catch (IndexOutOfBoundsException ioob) {
List<? extends Line> lines = lineCookie.getLineSet().getLines();
if (lines.size() > 0) {
return lines.get(lines.size() - 1);
} else {
return null;
}
}
}
private static final String[] KNOWN_SOURCE_TYPES = {Sources.TYPE_GENERIC,
WebClientProjectConstants.SOURCES_TYPE_HTML5_SITE_ROOT,
WebClientProjectConstants.SOURCES_TYPE_HTML5_TEST,
WebClientProjectConstants.SOURCES_TYPE_HTML5_TEST_SELENIUM };
private static FileObject findFileInProjecSources(Project project, String filePath) {
Sources sources = ProjectUtils.getSources(project);
for (String sourceType : KNOWN_SOURCE_TYPES) {
SourceGroup[] sourceGroups = sources.getSourceGroups(sourceType);
for (SourceGroup sourceGroup : sourceGroups) {
FileObject rootFolder = sourceGroup.getRootFolder();
FileObject fileObject = rootFolder.getFileObject(filePath);
if (fileObject != null) {
return fileObject;
}
}
}
return null;
}
public static LineCookie getLineCookie(final FileObject fo) {
LineCookie result = null;
try {
DataObject dataObject = DataObject.find(fo);
if (dataObject != null) {
result = dataObject.getLookup().lookup(LineCookie.class);
}
} catch (DataObjectNotFoundException e) {
Exceptions.printStackTrace(Exceptions.attachSeverity(e, Level.INFO));
}
return result;
}
private class ConsoleReader implements Runnable {
private final BufferedReader r;
private volatile boolean closing = false;
public ConsoleReader(Reader r) {
this.r = new BufferedReader(r);
}
void closing() {
closing = true;
}
@Override
public void run() {
try {
String line;
while (!closing) {
line = r.readLine();
LOG.log(Level.FINE, "Got line from Console Reader: \"{0}\"", line);
if (line != null) {
input.line(line);
}
}
} catch (IOException ex) {
if (!closing) {
Exceptions.printStackTrace(ex);
}
}
LOG.log(Level.FINE, "ConsoleReader has finished. Bye.");
}
}
}