c04f7064f54494d849119eba31e86eb543a2e294
[idea/community.git] / platform / platform-impl / src / com / intellij / notification / EventLog.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.notification;
18
19 import com.intellij.execution.filters.HyperlinkInfo;
20 import com.intellij.ide.actions.ContextHelpAction;
21 import com.intellij.notification.impl.NotificationsConfigurable;
22 import com.intellij.notification.impl.NotificationsConfigurationImpl;
23 import com.intellij.notification.impl.NotificationsManagerImpl;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.components.AbstractProjectComponent;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.Editor;
29 import com.intellij.openapi.editor.RangeMarker;
30 import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
31 import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
32 import com.intellij.openapi.editor.impl.DocumentImpl;
33 import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
34 import com.intellij.openapi.options.ShowSettingsUtil;
35 import com.intellij.openapi.project.DumbAware;
36 import com.intellij.openapi.project.DumbAwareAction;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.project.ProjectManager;
39 import com.intellij.openapi.ui.SimpleToolWindowPanel;
40 import com.intellij.openapi.ui.popup.Balloon;
41 import com.intellij.openapi.util.*;
42 import com.intellij.openapi.util.text.StringUtil;
43 import com.intellij.openapi.wm.ToolWindow;
44 import com.intellij.openapi.wm.ToolWindowFactory;
45 import com.intellij.openapi.wm.ToolWindowManager;
46 import com.intellij.ui.awt.RelativePoint;
47 import com.intellij.ui.content.Content;
48 import com.intellij.ui.content.ContentFactory;
49 import com.intellij.util.containers.hash.LinkedHashMap;
50 import com.intellij.util.text.CharArrayUtil;
51 import org.jetbrains.annotations.NonNls;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54
55 import javax.swing.event.HyperlinkEvent;
56 import java.net.MalformedURLException;
57 import java.net.URL;
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.concurrent.CopyOnWriteArrayList;
62 import java.util.concurrent.atomic.AtomicBoolean;
63 import java.util.regex.Matcher;
64 import java.util.regex.Pattern;
65
66 /**
67  * @author peter
68  */
69 public class EventLog implements Notifications {
70   public static final String LOG_REQUESTOR = "Internal log requestor";
71   public static final String LOG_TOOL_WINDOW_ID = "Event Log";
72   public static final String HELP_ID = "reference.toolwindows.event.log";
73   private final LogModel myModel = new LogModel(null, ApplicationManager.getApplication());
74   private static final String A_CLOSING = "</a>";
75   private static final Pattern TAG_PATTERN = Pattern.compile("<[^>]*>");
76   private static final Pattern A_PATTERN = Pattern.compile("<a ([^>]* )?href=[\"\']([^>]*)[\"\'][^>]*>");
77
78   public EventLog() {
79     ApplicationManager.getApplication().getMessageBus().connect().subscribe(Notifications.TOPIC, this);
80   }
81
82   @Override
83   public void notify(@NotNull Notification notification) {
84     final Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
85     if (openProjects.length == 0) {
86       myModel.addNotification(notification);
87     }
88     for (Project p : openProjects) {
89       getProjectComponent(p).printNotification(notification);
90     }
91   }
92
93   public static void expire(@NotNull Notification notification) {
94     getApplicationComponent().myModel.removeNotification(notification);
95     for (Project p : ProjectManager.getInstance().getOpenProjects()) {
96       getProjectComponent(p).myProjectModel.removeNotification(notification);
97     }
98   }
99
100   private static EventLog getApplicationComponent() {
101     return ApplicationManager.getApplication().getComponent(EventLog.class);
102   }
103
104   @Override
105   public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
106   }
107
108   @Override
109   public void register(@NotNull String groupDisplayName,
110                        @NotNull NotificationDisplayType defaultDisplayType,
111                        boolean shouldLog) {
112   }
113
114   @NotNull
115   public static LogModel getLogModel(@Nullable Project project) {
116     return project != null ? getProjectComponent(project).myProjectModel : getApplicationComponent().myModel;
117   }
118
119   @Nullable
120   public static Pair<Notification, Long> getStatusMessage(@Nullable Project project) {
121     return getLogModel(project).getStatusMessage();
122   }
123
124   public static LogEntry formatForLog(@NotNull final Notification notification) {
125     DocumentImpl document = new DocumentImpl(true);
126     AtomicBoolean showMore = new AtomicBoolean(false);
127     Map<RangeMarker, HyperlinkInfo> links = new LinkedHashMap<RangeMarker, HyperlinkInfo>();
128     List<RangeMarker> lineSeparators = new ArrayList<RangeMarker>();
129
130     boolean hasHtml = parseHtmlContent(notification, document, showMore, links, lineSeparators);
131     removeJavaNewLines(document, lineSeparators, hasHtml);
132     insertNewLineSubstitutors(document, showMore, lineSeparators);
133
134     String status = document.getText();
135
136     ArrayList<Pair<TextRange, HyperlinkInfo>> list = new ArrayList<Pair<TextRange, HyperlinkInfo>>();
137     for (RangeMarker marker : links.keySet()) {
138       if (!marker.isValid()) {
139         showMore.set(true);
140         continue;
141       }
142       list.add(Pair.create(new TextRange(marker.getStartOffset(), marker.getEndOffset()), links.get(marker)));
143     }
144
145     if (showMore.get()) {
146       String sb = "show balloon";
147       appendText(document, " (" + sb + ")");
148       list.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(document.getTextLength() - 1 - sb.length(), sb.length()),
149                                                   new ShowBalloon(notification)));
150     }
151
152     return new LogEntry(document.getText(), status, list);
153   }
154
155   private static boolean parseHtmlContent(Notification notification,
156                                           Document document,
157                                           AtomicBoolean showMore,
158                                           Map<RangeMarker, HyperlinkInfo> links, List<RangeMarker> lineSeparators) {
159     String content = notification.getContent();
160     String title = notification.getTitle();
161     if (StringUtil.isNotEmpty(title)) {
162       content = title + (StringUtil.isNotEmpty(content) ? ": " + content : "");
163     }
164
165     content = StringUtil.replace(StringUtil.convertLineSeparators(content), "&nbsp;", " ");
166     boolean hasHtml = false;
167     while (true) {
168       Matcher tagMatcher = TAG_PATTERN.matcher(content);
169       if (!tagMatcher.find()) {
170         appendText(document, content);
171         break;
172       }
173       
174       String tagStart = tagMatcher.group();
175       appendText(document, content.substring(0, tagMatcher.start()));
176       Matcher aMatcher = A_PATTERN.matcher(tagStart);
177       if (aMatcher.matches()) {
178         final String href = aMatcher.group(2);
179         int linkEnd = content.indexOf(A_CLOSING, tagMatcher.end());
180         if (linkEnd > 0) {
181           String linkText = content.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), "");
182           appendText(document, linkText);
183           links.put(document.createRangeMarker(new TextRange(document.getTextLength() - linkText.length(), document.getTextLength())),
184                     new NotificationHyperlinkInfo(notification, href));
185           content = content.substring(linkEnd + A_CLOSING.length());
186         }
187       }
188       else {
189         hasHtml = true;
190         if ("<br>".equals(tagStart) ||
191             "</br>".equals(tagStart) ||
192             "</p>".equals(tagStart) ||
193             "<p>".equals(tagStart) ||
194             "<p/>".equals(tagStart)) {
195           lineSeparators.add(document.createRangeMarker(TextRange.from(document.getTextLength(), 0)));
196         }
197         else if (!"<html>".equals(tagStart) && !"</html>".equals(tagStart) && !"<body>".equals(tagStart) && !"</body>".equals(tagStart)) {
198           showMore.set(true);
199         }
200         content = content.substring(tagMatcher.end());
201       }
202     }
203     return hasHtml;
204   }
205
206   private static void insertNewLineSubstitutors(Document document, AtomicBoolean showMore, List<RangeMarker> lineSeparators) {
207     for (int j = lineSeparators.size() - 1; j >= 0; j--) {
208       RangeMarker marker = lineSeparators.get(j);
209       if (!marker.isValid()) {
210         showMore.set(true);
211         continue;
212       }
213       
214       int offset = marker.getStartOffset();
215       if (offset == 0 || offset == document.getTextLength()) {
216         continue;
217       }
218       boolean spaceBefore = offset > 0 && Character.isWhitespace(document.getCharsSequence().charAt(offset - 1));
219       if (offset < document.getTextLength()) {
220         boolean spaceAfter = Character.isWhitespace(document.getCharsSequence().charAt(offset));
221         int next = CharArrayUtil.shiftForward(document.getCharsSequence(), offset, " \t");
222         if (next < document.getTextLength() && Character.isUpperCase(document.getCharsSequence().charAt(next))) {
223           document.insertString(offset, (spaceBefore ? "" : " ") + "//" + (spaceAfter ? "" : " "));
224           continue;
225         }
226         if (spaceAfter) {
227           continue;
228         }
229       }
230       if (spaceBefore) {
231         continue;
232       }
233
234       document.insertString(offset, " ");
235     }
236   }
237
238   private static void removeJavaNewLines(Document document, List<RangeMarker> lineSeparators, boolean hasHtml) {
239     String text = document.getText();
240     int i = -1;
241     while (true) {
242       i = text.indexOf('\n', i + 1);
243       if (i < 0) break;
244       document.deleteString(i, i + 1);
245       if (!hasHtml) {
246         lineSeparators.add(document.createRangeMarker(TextRange.from(i, 0)));
247       }
248     }
249   }
250
251   private static void appendText(Document document, String text) {
252     document.insertString(document.getTextLength(), StringUtil.unescapeXml(text));
253   }
254
255   public static class LogEntry {
256     public final String message;
257     public final String status;
258     public final List<Pair<TextRange, HyperlinkInfo>> links;
259
260     public LogEntry(String message, String status, List<Pair<TextRange, HyperlinkInfo>> links) {
261       this.message = message;
262       this.status = status;
263       this.links = links;
264     }
265   }
266
267   @Nullable
268   public static ToolWindow getEventLog(Project project) {
269     return project == null ? null : ToolWindowManager.getInstance(project).getToolWindow(LOG_TOOL_WINDOW_ID);
270   }
271
272   public static void toggleLog(final Project project) {
273     final ToolWindow eventLog = getEventLog(project);
274     if (eventLog != null) {
275       if (!eventLog.isVisible()) {
276         eventLog.activate(null, true);
277         getLogModel(project).logShown();
278       } else {
279         eventLog.hide(null);
280       }
281     }
282   }
283
284   public static class ProjectTracker extends AbstractProjectComponent {
285     private volatile EventLogConsole myConsole;
286     private final List<Notification> myInitial = new CopyOnWriteArrayList<Notification>();
287     private final LogModel myProjectModel;
288
289     public ProjectTracker(@NotNull final Project project) {
290       super(project);
291
292       myProjectModel = new LogModel(project, project);
293
294       for (Notification notification : getApplicationComponent().myModel.takeNotifications()) {
295         printNotification(notification);
296       }
297
298       project.getMessageBus().connect(project).subscribe(Notifications.TOPIC, new Notifications() {
299         @Override
300         public void notify(@NotNull Notification notification) {
301           printNotification(notification);
302         }
303
304         @Override
305         public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
306         }
307
308         @Override
309         public void register(@NotNull String groupDisplayName,
310                              @NotNull NotificationDisplayType defaultDisplayType,
311                              boolean shouldLog) {
312         }
313       });
314
315     }
316
317     @Override
318     public void projectOpened() {
319       myConsole = new EventLogConsole(myProjectModel);
320
321       for (Notification notification : myInitial) {
322         printNotification(notification);
323       }
324       myInitial.clear();
325     }
326
327     @Override
328     public void projectClosed() {
329       getApplicationComponent().myModel.setStatusMessage(null, 0);
330     }
331
332     private void printNotification(final Notification notification) {
333       final EventLogConsole console = myConsole;
334       if (console == null) {
335         myInitial.add(notification);
336         return;
337       }
338
339       if (!NotificationsConfigurationImpl.getSettings(notification.getGroupId()).isShouldLog()) {
340         return;
341       }
342
343       myProjectModel.addNotification(notification);
344
345       ApplicationManager.getApplication().invokeLater(new Runnable() {
346         @Override
347         public void run() {
348           if (!ShutDownTracker.isShutdownHookRunning() && !myProject.isDisposed()) {
349             console.doPrintNotification(notification);
350           }
351         }
352       });
353     }
354
355   }
356
357   private static ProjectTracker getProjectComponent(Project project) {
358     return project.getComponent(ProjectTracker.class);
359   }
360   public static class FactoryItself implements ToolWindowFactory, DumbAware {
361     public void createToolWindowContent(final Project project, ToolWindow toolWindow) {
362       final Editor editor = getProjectComponent(project).myConsole.getConsoleEditor();
363
364       SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true) {
365         @Override
366         public Object getData(@NonNls String dataId) {
367           return PlatformDataKeys.HELP_ID.is(dataId) ? HELP_ID : super.getData(dataId);
368         }
369       };
370       panel.setContent(editor.getComponent());
371
372       DefaultActionGroup group = new DefaultActionGroup();
373       group.add(new DumbAwareAction("Settings", "Edit notification settings", IconLoader.getIcon("/actions/showSettings.png")) {
374         @Override
375         public void actionPerformed(AnActionEvent e) {
376           ShowSettingsUtil.getInstance().editConfigurable(project, new NotificationsConfigurable());
377         }
378       });
379       group.add(new DisplayBalloons());
380       group.add(new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
381         @Override
382         protected Editor getEditor(AnActionEvent e) {
383           return editor;
384         }
385       });
386       group.add(new ScrollToTheEndToolbarAction(editor));
387       group.add(new DumbAwareAction("Mark all as read", "Mark all unread notifications as read", IconLoader.getIcon("/general/reset.png")) {
388         @Override
389         public void update(AnActionEvent e) {
390           if (project.isDisposed()) return;
391           e.getPresentation().setEnabled(!getProjectComponent(project).myProjectModel.getNotifications().isEmpty());
392         }
393
394         @Override
395         public void actionPerformed(AnActionEvent e) {
396           LogModel model = getProjectComponent(project).myProjectModel;
397           for (Notification notification : model.getNotifications()) {
398             model.removeNotification(notification);
399             notification.expire();
400           }
401         }
402       });
403       group.add(new ContextHelpAction(HELP_ID));
404
405       ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, false);
406       toolbar.setTargetComponent(panel);
407       panel.setToolbar(toolbar.getComponent());
408
409       final Content content = ContentFactory.SERVICE.getInstance().createContent(panel, "", false);
410       toolWindow.getContentManager().addContent(content);
411     }
412
413     private static class DisplayBalloons extends ToggleAction implements DumbAware {
414       public DisplayBalloons() {
415         super("Show balloons", "Enable or suppress notification balloons", IconLoader.getIcon("/general/balloon.png"));
416       }
417
418       @Override
419       public boolean isSelected(AnActionEvent e) {
420         return NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS;
421       }
422
423       @Override
424       public void setSelected(AnActionEvent e, boolean state) {
425         NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS = state;
426       }
427     }
428   }
429
430   private static class NotificationHyperlinkInfo implements HyperlinkInfo {
431     private final Notification myNotification;
432     private final String myHref;
433
434     public NotificationHyperlinkInfo(Notification notification, String href) {
435       myNotification = notification;
436       myHref = href;
437     }
438
439     @Override
440     public void navigate(Project project) {
441       NotificationListener listener = myNotification.getListener();
442       if (listener != null) {
443         EventLogConsole console = EventLog.getProjectComponent(project).myConsole;
444         URL url = null;
445         try {
446           url = new URL(null, myHref);
447         }
448         catch (MalformedURLException ignored) {
449         }
450         listener.hyperlinkUpdate(myNotification, new HyperlinkEvent(console.getConsoleEditor().getContentComponent(), HyperlinkEvent.EventType.ACTIVATED, url, myHref));
451       }
452     }
453   }
454
455   private static class ShowBalloon implements HyperlinkInfo {
456     private final Notification myNotification;
457
458     public ShowBalloon(Notification notification) {
459       myNotification = notification;
460     }
461
462     @Override
463     public void navigate(Project project) {
464       hideBalloon(myNotification);
465
466       for (Notification notification : getLogModel(project).getNotifications()) {
467         hideBalloon(notification);
468       }
469
470       RelativePoint target = EventLog.getProjectComponent(project).myConsole.getHyperlinkLocation(this);
471       if (target != null) {
472         Balloon balloon = NotificationsManagerImpl.createBalloon(myNotification, true, true);
473         Disposer.register(project, balloon);
474         balloon.show(target, Balloon.Position.above);
475       }
476     }
477
478     private static void hideBalloon(Notification notification1) {
479       Balloon balloon = notification1.getBalloon();
480       if (balloon != null) {
481         balloon.hide();
482       }
483     }
484   }
485 }