| /* |
| * Copyright 2000-2012 JetBrains s.r.o. |
| * |
| * Licensed 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 com.intellij.notification; |
| |
| import com.intellij.execution.filters.HyperlinkInfo; |
| import com.intellij.notification.impl.NotificationsConfigurationImpl; |
| import com.intellij.notification.impl.NotificationsManagerImpl; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.AbstractProjectComponent; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.RangeMarker; |
| import com.intellij.openapi.editor.impl.DocumentImpl; |
| import com.intellij.openapi.editor.markup.RangeHighlighter; |
| import com.intellij.openapi.project.DumbAwareRunnable; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.project.ProjectManager; |
| import com.intellij.openapi.startup.StartupManager; |
| import com.intellij.openapi.ui.popup.Balloon; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.wm.*; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.ui.content.Content; |
| import com.intellij.util.IJSwingUtilities; |
| import com.intellij.util.ObjectUtils; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.hash.LinkedHashMap; |
| import com.intellij.util.text.CharArrayUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.util.*; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * @author peter |
| */ |
| public class EventLog { |
| public static final String LOG_REQUESTOR = "Internal log requestor"; |
| public static final String LOG_TOOL_WINDOW_ID = "Event Log"; |
| public static final String HELP_ID = "reference.toolwindows.event.log"; |
| private static final String A_CLOSING = "</a>"; |
| private static final Pattern TAG_PATTERN = Pattern.compile("<[^>]*>"); |
| private static final Pattern A_PATTERN = Pattern.compile("<a ([^>]* )?href=[\"\']([^>]*)[\"\'][^>]*>"); |
| private static final Set<String> NEW_LINES = ContainerUtil.newHashSet("<br>", "</br>", "<br/>", "<p>", "</p>", "<p/>"); |
| private static final String DEFAULT_CATEGORY = ""; |
| |
| private final LogModel myModel = new LogModel(null, ApplicationManager.getApplication()); |
| |
| public EventLog() { |
| ApplicationManager.getApplication().getMessageBus().connect().subscribe(Notifications.TOPIC, new NotificationsAdapter() { |
| @Override |
| public void notify(@NotNull Notification notification) { |
| final Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); |
| if (openProjects.length == 0) { |
| myModel.addNotification(notification); |
| } |
| for (Project p : openProjects) { |
| getProjectComponent(p).printNotification(notification); |
| } |
| } |
| }); |
| } |
| |
| public static void expireNotification(@NotNull Notification notification) { |
| getApplicationComponent().myModel.removeNotification(notification); |
| for (Project p : ProjectManager.getInstance().getOpenProjects()) { |
| getProjectComponent(p).myProjectModel.removeNotification(notification); |
| } |
| } |
| |
| private static EventLog getApplicationComponent() { |
| return ApplicationManager.getApplication().getComponent(EventLog.class); |
| } |
| |
| @NotNull |
| public static LogModel getLogModel(@Nullable Project project) { |
| return project != null ? getProjectComponent(project).myProjectModel : getApplicationComponent().myModel; |
| } |
| |
| @Nullable |
| public static Trinity<Notification, String, Long> getStatusMessage(@Nullable Project project) { |
| return getLogModel(project).getStatusMessage(); |
| } |
| |
| public static LogEntry formatForLog(@NotNull final Notification notification, String indent) { |
| DocumentImpl logDoc = new DocumentImpl("",true); |
| AtomicBoolean showMore = new AtomicBoolean(false); |
| Map<RangeMarker, HyperlinkInfo> links = new LinkedHashMap<RangeMarker, HyperlinkInfo>(); |
| List<RangeMarker> lineSeparators = new ArrayList<RangeMarker>(); |
| |
| String title = notification.getTitle(); |
| String content = notification.getContent(); |
| RangeMarker afterTitle = null; |
| boolean hasHtml = parseHtmlContent(title, notification, logDoc, showMore, links, lineSeparators); |
| if (StringUtil.isNotEmpty(title)) { |
| if (StringUtil.isNotEmpty(content)) { |
| appendText(logDoc, ": "); |
| afterTitle = logDoc.createRangeMarker(logDoc.getTextLength() - 2, logDoc.getTextLength()); |
| } |
| } |
| hasHtml |= parseHtmlContent(content, notification, logDoc, showMore, links, lineSeparators); |
| |
| String status = getStatusText(logDoc, showMore, lineSeparators, hasHtml); |
| |
| indentNewLines(logDoc, lineSeparators, afterTitle, hasHtml, indent); |
| |
| ArrayList<Pair<TextRange, HyperlinkInfo>> list = new ArrayList<Pair<TextRange, HyperlinkInfo>>(); |
| for (RangeMarker marker : links.keySet()) { |
| if (!marker.isValid()) { |
| showMore.set(true); |
| continue; |
| } |
| list.add(Pair.create(new TextRange(marker.getStartOffset(), marker.getEndOffset()), links.get(marker))); |
| } |
| |
| if (showMore.get()) { |
| String sb = "show balloon"; |
| if (!logDoc.getText().endsWith(" ")) { |
| appendText(logDoc, " "); |
| } |
| appendText(logDoc, "(" + sb + ")"); |
| list.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(logDoc.getTextLength() - 1 - sb.length(), sb.length()), |
| new ShowBalloon(notification))); |
| } |
| |
| return new LogEntry(logDoc.getText(), status, list); |
| } |
| |
| private static void indentNewLines(DocumentImpl logDoc, List<RangeMarker> lineSeparators, RangeMarker afterTitle, boolean hasHtml, String indent) { |
| if (!hasHtml) { |
| int i = -1; |
| while (true) { |
| i = StringUtil.indexOf(logDoc.getText(), '\n', i + 1); |
| if (i < 0) { |
| break; |
| } |
| lineSeparators.add(logDoc.createRangeMarker(i, i + 1)); |
| } |
| } |
| if (!lineSeparators.isEmpty() && afterTitle != null && afterTitle.isValid()) { |
| lineSeparators.add(afterTitle); |
| } |
| int nextLineStart = -1; |
| for (RangeMarker separator : lineSeparators) { |
| if (separator.isValid()) { |
| int start = separator.getStartOffset(); |
| if (start == nextLineStart) { |
| continue; |
| } |
| |
| logDoc.replaceString(start, separator.getEndOffset(), "\n" + indent); |
| nextLineStart = start + 1 + indent.length(); |
| while (nextLineStart < logDoc.getTextLength() && Character.isWhitespace(logDoc.getCharsSequence().charAt(nextLineStart))) { |
| logDoc.deleteString(nextLineStart, nextLineStart + 1); |
| } |
| } |
| } |
| } |
| |
| private static String getStatusText(DocumentImpl logDoc, AtomicBoolean showMore, List<RangeMarker> lineSeparators, boolean hasHtml) { |
| DocumentImpl statusDoc = new DocumentImpl(logDoc.getImmutableCharSequence(),true); |
| List<RangeMarker> statusSeparators = new ArrayList<RangeMarker>(); |
| for (RangeMarker separator : lineSeparators) { |
| if (separator.isValid()) { |
| statusSeparators.add(statusDoc.createRangeMarker(separator.getStartOffset(), separator.getEndOffset())); |
| } |
| } |
| removeJavaNewLines(statusDoc, statusSeparators, hasHtml); |
| insertNewLineSubstitutors(statusDoc, showMore, statusSeparators); |
| |
| return statusDoc.getText(); |
| } |
| |
| private static boolean parseHtmlContent(String text, Notification notification, |
| Document document, |
| AtomicBoolean showMore, |
| Map<RangeMarker, HyperlinkInfo> links, List<RangeMarker> lineSeparators) { |
| String content = StringUtil.convertLineSeparators(text); |
| |
| int initialLen = document.getTextLength(); |
| boolean hasHtml = false; |
| while (true) { |
| Matcher tagMatcher = TAG_PATTERN.matcher(content); |
| if (!tagMatcher.find()) { |
| appendText(document, content); |
| break; |
| } |
| |
| String tagStart = tagMatcher.group(); |
| appendText(document, content.substring(0, tagMatcher.start())); |
| Matcher aMatcher = A_PATTERN.matcher(tagStart); |
| if (aMatcher.matches()) { |
| final String href = aMatcher.group(2); |
| int linkEnd = content.indexOf(A_CLOSING, tagMatcher.end()); |
| if (linkEnd > 0) { |
| String linkText = content.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), ""); |
| int linkStart = document.getTextLength(); |
| appendText(document, linkText); |
| links.put(document.createRangeMarker(new TextRange(linkStart, document.getTextLength())), |
| new NotificationHyperlinkInfo(notification, href)); |
| content = content.substring(linkEnd + A_CLOSING.length()); |
| continue; |
| } |
| } |
| |
| hasHtml = true; |
| if (NEW_LINES.contains(tagStart)) { |
| if (initialLen != document.getTextLength()) { |
| lineSeparators.add(document.createRangeMarker(TextRange.from(document.getTextLength(), 0))); |
| } |
| } |
| else if (!"<html>".equals(tagStart) && !"</html>".equals(tagStart) && !"<body>".equals(tagStart) && !"</body>".equals(tagStart)) { |
| showMore.set(true); |
| } |
| content = content.substring(tagMatcher.end()); |
| } |
| for (Iterator<RangeMarker> iterator = lineSeparators.iterator(); iterator.hasNext(); ) { |
| RangeMarker next = iterator.next(); |
| if (next.getEndOffset() == document.getTextLength()) { |
| iterator.remove(); |
| } |
| } |
| return hasHtml; |
| } |
| |
| private static void insertNewLineSubstitutors(Document document, AtomicBoolean showMore, List<RangeMarker> lineSeparators) { |
| for (RangeMarker marker : lineSeparators) { |
| if (!marker.isValid()) { |
| showMore.set(true); |
| continue; |
| } |
| |
| int offset = marker.getStartOffset(); |
| if (offset == 0 || offset == document.getTextLength()) { |
| continue; |
| } |
| boolean spaceBefore = offset > 0 && Character.isWhitespace(document.getCharsSequence().charAt(offset - 1)); |
| if (offset < document.getTextLength()) { |
| boolean spaceAfter = Character.isWhitespace(document.getCharsSequence().charAt(offset)); |
| int next = CharArrayUtil.shiftForward(document.getCharsSequence(), offset, " \t"); |
| if (next < document.getTextLength() && !Character.isLowerCase(document.getCharsSequence().charAt(next))) { |
| document.insertString(offset, (spaceBefore ? "" : " ") + "//" + (spaceAfter ? "" : " ")); |
| continue; |
| } |
| if (spaceAfter) { |
| continue; |
| } |
| } |
| if (spaceBefore) { |
| continue; |
| } |
| |
| document.insertString(offset, " "); |
| } |
| } |
| |
| private static void removeJavaNewLines(Document document, List<RangeMarker> lineSeparators, boolean hasHtml) { |
| CharSequence text = document.getCharsSequence(); |
| int i = 0; |
| while (true) { |
| i = StringUtil.indexOf(text, '\n', i); |
| if (i < 0) break; |
| document.deleteString(i, i + 1); |
| if (!hasHtml) { |
| lineSeparators.add(document.createRangeMarker(TextRange.from(i, 0))); |
| } |
| } |
| } |
| |
| private static void appendText(Document document, String text) { |
| text = StringUtil.replace(text, " ", " "); |
| text = StringUtil.replace(text, "»", ">>"); |
| text = StringUtil.replace(text, "«", "<<"); |
| text = StringUtil.replace(text, "…", "..."); |
| document.insertString(document.getTextLength(), StringUtil.unescapeXml(text)); |
| } |
| |
| public static class LogEntry { |
| public final String message; |
| public final String status; |
| public final List<Pair<TextRange, HyperlinkInfo>> links; |
| |
| public LogEntry(String message, String status, List<Pair<TextRange, HyperlinkInfo>> links) { |
| this.message = message; |
| this.status = status; |
| this.links = links; |
| } |
| } |
| |
| @Nullable |
| public static ToolWindow getEventLog(Project project) { |
| return project == null ? null : ToolWindowManager.getInstance(project).getToolWindow(LOG_TOOL_WINDOW_ID); |
| } |
| |
| public static void toggleLog(@Nullable final Project project, @Nullable final Notification notification) { |
| final ToolWindow eventLog = getEventLog(project); |
| if (eventLog != null) { |
| if (!eventLog.isVisible()) { |
| eventLog.activate(new Runnable() { |
| @Override |
| public void run() { |
| if (notification == null) return; |
| String contentName = getContentName(notification); |
| Content content = eventLog.getContentManager().findContent(contentName); |
| if (content != null) { |
| eventLog.getContentManager().setSelectedContent(content); |
| } |
| } |
| }, true); |
| } |
| else { |
| eventLog.hide(null); |
| } |
| } |
| } |
| |
| public static class ProjectTracker extends AbstractProjectComponent { |
| private final Map<String, EventLogConsole> myCategoryMap = ContainerUtil.newConcurrentMap(); |
| private final List<Notification> myInitial = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final LogModel myProjectModel; |
| |
| public ProjectTracker(@NotNull final Project project) { |
| super(project); |
| |
| myProjectModel = new LogModel(project, project); |
| |
| for (Notification notification : getApplicationComponent().myModel.takeNotifications()) { |
| printNotification(notification); |
| } |
| |
| project.getMessageBus().connect(project).subscribe(Notifications.TOPIC, new NotificationsAdapter() { |
| @Override |
| public void notify(@NotNull Notification notification) { |
| printNotification(notification); |
| } |
| }); |
| } |
| |
| void initDefaultContent() { |
| createNewContent(DEFAULT_CATEGORY); |
| |
| for (Notification notification : myInitial) { |
| doPrintNotification(notification, ObjectUtils.assertNotNull(getConsole(notification))); |
| } |
| myInitial.clear(); |
| } |
| |
| @Override |
| public void projectOpened() { |
| } |
| |
| @Override |
| public void projectClosed() { |
| getApplicationComponent().myModel.setStatusMessage(null, 0); |
| StatusBar.Info.set("", null, LOG_REQUESTOR); |
| } |
| |
| private void printNotification(Notification notification) { |
| if (!NotificationsConfigurationImpl.getSettings(notification.getGroupId()).isShouldLog()) { |
| return; |
| } |
| myProjectModel.addNotification(notification); |
| |
| EventLogConsole console = getConsole(notification); |
| if (console == null) { |
| myInitial.add(notification); |
| } |
| else { |
| doPrintNotification(notification, console); |
| } |
| } |
| |
| private void doPrintNotification(@NotNull final Notification notification, @NotNull final EventLogConsole console) { |
| StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() { |
| @Override |
| public void run() { |
| if (!ShutDownTracker.isShutdownHookRunning() && !myProject.isDisposed()) { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| public void run() { |
| console.doPrintNotification(notification); |
| } |
| }); |
| } |
| } |
| }); |
| } |
| |
| @Nullable |
| private EventLogConsole getConsole(Notification notification) { |
| if (myCategoryMap.get(DEFAULT_CATEGORY) == null) return null; // still not initialized |
| |
| String name = getContentName(notification); |
| EventLogConsole console = myCategoryMap.get(name); |
| return console != null ? console : createNewContent(name); |
| } |
| |
| @NotNull |
| private EventLogConsole createNewContent(String name) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| EventLogConsole newConsole = new EventLogConsole(myProjectModel); |
| EventLogToolWindowFactory.createContent(myProject, getEventLog(myProject), newConsole, name); |
| myCategoryMap.put(name, newConsole); |
| |
| return newConsole; |
| } |
| |
| } |
| |
| @NotNull |
| private static String getContentName(Notification notification) { |
| for (EventLogCategory category : EventLogCategory.EP_NAME.getExtensions()) { |
| if (category.acceptsNotification(notification.getGroupId())) { |
| return category.getDisplayName(); |
| } |
| } |
| return DEFAULT_CATEGORY; |
| } |
| |
| static ProjectTracker getProjectComponent(Project project) { |
| return project.getComponent(ProjectTracker.class); |
| } |
| |
| private static class NotificationHyperlinkInfo implements HyperlinkInfo { |
| private final Notification myNotification; |
| private final String myHref; |
| |
| public NotificationHyperlinkInfo(Notification notification, String href) { |
| myNotification = notification; |
| myHref = href; |
| } |
| |
| @Override |
| public void navigate(Project project) { |
| NotificationListener listener = myNotification.getListener(); |
| if (listener != null) { |
| EventLogConsole console = ObjectUtils.assertNotNull(getProjectComponent(project).getConsole(myNotification)); |
| JComponent component = console.getConsoleEditor().getContentComponent(); |
| listener.hyperlinkUpdate(myNotification, IJSwingUtilities.createHyperlinkEvent(myHref, component)); |
| } |
| } |
| } |
| |
| static class ShowBalloon implements HyperlinkInfo { |
| private final Notification myNotification; |
| private RangeHighlighter myRangeHighlighter; |
| |
| public ShowBalloon(Notification notification) { |
| myNotification = notification; |
| } |
| |
| public void setRangeHighlighter(RangeHighlighter rangeHighlighter) { |
| myRangeHighlighter = rangeHighlighter; |
| } |
| |
| @Override |
| public void navigate(Project project) { |
| hideBalloon(myNotification); |
| |
| for (Notification notification : getLogModel(project).getNotifications()) { |
| hideBalloon(notification); |
| } |
| |
| EventLogConsole console = ObjectUtils.assertNotNull(getProjectComponent(project).getConsole(myNotification)); |
| if (myRangeHighlighter == null || !myRangeHighlighter.isValid()) { |
| return; |
| } |
| RelativePoint target = console.getRangeHighlighterLocation(myRangeHighlighter); |
| if (target != null) { |
| IdeFrame frame = WindowManager.getInstance().getIdeFrame(project); |
| assert frame != null; |
| Balloon balloon = NotificationsManagerImpl.createBalloon(frame, myNotification, true, true); |
| Disposer.register(project, balloon); |
| balloon.show(target, Balloon.Position.above); |
| } |
| } |
| |
| private static void hideBalloon(Notification notification1) { |
| Balloon balloon = notification1.getBalloon(); |
| if (balloon != null) { |
| balloon.hide(true); |
| } |
| } |
| } |
| } |