9ca1bd12f697ae62f6beca8a23de1fa8939910ba
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / ui / breakpoints / LineBreakpoint.java
1 /*
2  * Copyright 2000-2009 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 /*
18  * Class LineBreakpoint
19  * @author Jeka
20  */
21 package com.intellij.debugger.ui.breakpoints;
22
23 import com.intellij.debugger.DebuggerBundle;
24 import com.intellij.debugger.DebuggerManagerEx;
25 import com.intellij.debugger.SourcePosition;
26 import com.intellij.debugger.actions.ThreadDumpAction;
27 import com.intellij.debugger.engine.DebugProcessImpl;
28 import com.intellij.debugger.engine.evaluation.EvaluateException;
29 import com.intellij.debugger.engine.evaluation.EvaluationContextImpl;
30 import com.intellij.debugger.impl.DebuggerUtilsEx;
31 import com.intellij.debugger.impl.PositionUtil;
32 import com.intellij.debugger.jdi.StackFrameProxyImpl;
33 import com.intellij.icons.AllIcons;
34 import com.intellij.openapi.application.ApplicationManager;
35 import com.intellij.openapi.diagnostic.Logger;
36 import com.intellij.openapi.editor.Document;
37 import com.intellij.openapi.editor.markup.RangeHighlighter;
38 import com.intellij.openapi.fileEditor.FileDocumentManager;
39 import com.intellij.openapi.module.Module;
40 import com.intellij.openapi.project.Project;
41 import com.intellij.openapi.roots.ProjectFileIndex;
42 import com.intellij.openapi.roots.ProjectRootManager;
43 import com.intellij.openapi.util.Computable;
44 import com.intellij.openapi.util.Key;
45 import com.intellij.openapi.util.registry.Registry;
46 import com.intellij.openapi.vfs.VirtualFile;
47 import com.intellij.psi.*;
48 import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
49 import com.intellij.psi.jsp.JspFile;
50 import com.intellij.psi.search.EverythingGlobalScope;
51 import com.intellij.psi.search.GlobalSearchScope;
52 import com.intellij.psi.util.PsiTreeUtil;
53 import com.intellij.ui.classFilter.ClassFilter;
54 import com.intellij.util.Function;
55 import com.intellij.util.Processor;
56 import com.intellij.util.StringBuilderSpinAllocator;
57 import com.intellij.util.containers.ContainerUtil;
58 import com.intellij.xdebugger.XDebuggerUtil;
59 import com.sun.jdi.*;
60 import com.sun.jdi.event.LocatableEvent;
61 import com.sun.jdi.request.BreakpointRequest;
62 import org.jetbrains.annotations.NonNls;
63 import org.jetbrains.annotations.Nullable;
64
65 import javax.swing.*;
66 import java.util.ArrayList;
67 import java.util.Collection;
68 import java.util.List;
69
70 public class LineBreakpoint extends BreakpointWithHighlighter {
71   private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.breakpoints.LineBreakpoint");
72
73   private String myMethodName;
74   public static final @NonNls Key<LineBreakpoint> CATEGORY = BreakpointCategory.lookup("line_breakpoints");
75
76   protected LineBreakpoint(Project project) {
77     super(project);
78   }
79
80   protected LineBreakpoint(Project project, RangeHighlighter highlighter) {
81     super(project, highlighter);
82   }
83
84   protected Icon getDisabledIcon(boolean isMuted) {
85     final Breakpoint master = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().findMasterBreakpoint(this);
86     if (isMuted) {
87       return master == null? AllIcons.Debugger.Db_muted_disabled_breakpoint : AllIcons.Debugger.Db_muted_dep_line_breakpoint;
88     }
89     else {
90       return master == null? AllIcons.Debugger.Db_disabled_breakpoint : AllIcons.Debugger.Db_dep_line_breakpoint;
91     }
92   }
93
94   protected Icon getSetIcon(boolean isMuted) {
95     if (REMOVE_AFTER_HIT) {
96       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
97     }
98     return isMuted? AllIcons.Debugger.Db_muted_breakpoint : AllIcons.Debugger.Db_set_breakpoint;
99   }
100
101   protected Icon getInvalidIcon(boolean isMuted) {
102     return isMuted? AllIcons.Debugger.Db_muted_invalid_breakpoint : AllIcons.Debugger.Db_invalid_breakpoint;
103   }
104
105   protected Icon getVerifiedIcon(boolean isMuted) {
106     if (REMOVE_AFTER_HIT) {
107       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
108     }
109     return isMuted? AllIcons.Debugger.Db_muted_verified_breakpoint : AllIcons.Debugger.Db_verified_breakpoint;
110   }
111
112   protected Icon getVerifiedWarningsIcon(boolean isMuted) {
113     return isMuted? AllIcons.Debugger.Db_muted_verified_warning_breakpoint : AllIcons.Debugger.Db_verified_warning_breakpoint;
114   }
115
116   public Key<LineBreakpoint> getCategory() {
117     return CATEGORY;
118   }
119
120   protected void reload(PsiFile file) {
121     super.reload(file);
122     myMethodName = findMethodName(file, getHighlighter().getStartOffset());
123   }
124
125   protected void createOrWaitPrepare(DebugProcessImpl debugProcess, String classToBeLoaded) {
126     if (isInScopeOf(debugProcess, classToBeLoaded)) {
127       super.createOrWaitPrepare(debugProcess, classToBeLoaded);
128     }
129   }
130
131   protected void createRequestForPreparedClass(final DebugProcessImpl debugProcess, final ReferenceType classType) {
132     if (!isInScopeOf(debugProcess, classType.name())) {
133       if (LOG.isDebugEnabled()) {
134         LOG.debug(classType.name() + " is out of debug-process scope, breakpoint request won't be created for line " + getLineIndex());
135       }
136       return;
137     }
138     try {
139       List<Location> locs = debugProcess.getPositionManager().locationsOfLine(classType, getSourcePosition());
140       if (!locs.isEmpty()) {
141         for (Location loc : locs) {
142           if (LOG.isDebugEnabled()) {
143             LOG.debug("Found location [codeIndex=" + loc.codeIndex() +"] for reference type " + classType.name() + " at line " + getLineIndex() + "; isObsolete: " + (debugProcess.getVirtualMachineProxy().versionHigher("1.4") && loc.method().isObsolete()));
144           }
145           BreakpointRequest request = debugProcess.getRequestsManager().createBreakpointRequest(LineBreakpoint.this, loc);
146           debugProcess.getRequestsManager().enableRequest(request);
147           if (LOG.isDebugEnabled()) {
148             LOG.debug("Created breakpoint request for reference type " + classType.name() + " at line " + getLineIndex() + "; codeIndex=" + loc.codeIndex());
149           }
150         }
151       }
152       else {
153         // there's no executable code in this class
154         debugProcess.getRequestsManager().setInvalid(LineBreakpoint.this, DebuggerBundle.message(
155           "error.invalid.breakpoint.no.executable.code", (getLineIndex() + 1), classType.name())
156         );
157         if (LOG.isDebugEnabled()) {
158           LOG.debug("No locations of type " + classType.name() + " found at line " + getLineIndex());
159         }
160       }
161     }
162     catch (ClassNotPreparedException ex) {
163       if (LOG.isDebugEnabled()) {
164         LOG.debug("ClassNotPreparedException: " + ex.getMessage());
165       }
166       // there's a chance to add a breakpoint when the class is prepared
167     }
168     catch (ObjectCollectedException ex) {
169       if (LOG.isDebugEnabled()) {
170         LOG.debug("ObjectCollectedException: " + ex.getMessage());
171       }
172       // there's a chance to add a breakpoint when the class is prepared
173     }
174     catch (InvalidLineNumberException ex) {
175       if (LOG.isDebugEnabled()) {
176         LOG.debug("InvalidLineNumberException: " + ex.getMessage());
177       }
178       debugProcess.getRequestsManager().setInvalid(LineBreakpoint.this, DebuggerBundle.message("error.invalid.breakpoint.bad.line.number"));
179     }
180     catch (InternalException ex) {
181       LOG.info(ex);
182     }
183     catch(Exception ex) {
184       LOG.info(ex);
185     }
186     updateUI();
187   }
188
189   private boolean isInScopeOf(DebugProcessImpl debugProcess, String className) {
190     final SourcePosition position = getSourcePosition();
191     if (position != null) {
192       final VirtualFile breakpointFile = position.getFile().getVirtualFile();
193       final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
194       if (breakpointFile != null && fileIndex.isInSourceContent(breakpointFile)) {
195         // apply filtering to breakpoints from content sources only, not for sources attached to libraries
196         final Collection<VirtualFile> candidates = findClassCandidatesInSourceContent(className, debugProcess.getSearchScope(), fileIndex);
197         if (LOG.isDebugEnabled()) {
198           LOG.debug("Found "+ (candidates == null? "null" : candidates.size()) + " candidate containing files for class " + className);
199         }
200         if (candidates == null) {
201           return true;
202         }
203         for (VirtualFile classFile : candidates) {
204           if (LOG.isDebugEnabled()) {
205             LOG.debug("Breakpoint file: " + breakpointFile.getPath()+ "; candidate file: " + classFile.getPath());
206           }
207           if (breakpointFile.equals(classFile)) {
208             return true;
209           }
210         }
211         if (LOG.isDebugEnabled()) {
212           final GlobalSearchScope scope = debugProcess.getSearchScope();
213           final boolean contains = scope.contains(breakpointFile);
214           final Project project = getProject();
215           final List<VirtualFile> files = ContainerUtil.map(
216             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, scope), new Function<PsiClass, VirtualFile>() {
217             @Override
218             public VirtualFile fun(PsiClass aClass) {
219               return aClass.getContainingFile().getVirtualFile();
220             }
221           });
222           final List<VirtualFile> allFiles = ContainerUtil.map(
223             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, new EverythingGlobalScope(project)), new Function<PsiClass, VirtualFile>() {
224             @Override
225             public VirtualFile fun(PsiClass aClass) {
226               return aClass.getContainingFile().getVirtualFile();
227             }
228           });
229           final VirtualFile contentRoot = fileIndex.getContentRootForFile(breakpointFile);
230           final Module module = fileIndex.getModuleForFile(breakpointFile);
231
232           LOG.debug("Did not find '" +
233                     className + "' in " + scope +
234                     "; contains=" + contains +
235                     "; contentRoot=" + contentRoot +
236                     "; module = " + module +
237                     "; all files in index are: " + files+
238                     "; all possible files are: " + allFiles
239           );
240         }
241         
242         return false;
243       }
244     }
245     return true;
246   }
247
248   @Nullable
249   private Collection<VirtualFile> findClassCandidatesInSourceContent(final String className, final GlobalSearchScope scope, final ProjectFileIndex fileIndex) {
250     final int dollarIndex = className.indexOf("$");
251     final String topLevelClassName = dollarIndex >= 0? className.substring(0, dollarIndex) : className;
252     return ApplicationManager.getApplication().runReadAction(new Computable<Collection<VirtualFile>>() {
253       @Nullable
254       public Collection<VirtualFile> compute() {
255         final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(topLevelClassName, scope);
256         if (LOG.isDebugEnabled()) {
257           LOG.debug("Found "+ classes.length + " classes " + topLevelClassName + " in scope "+scope);
258         }
259         if (classes.length == 0) {
260           return null;
261         }
262         final List<VirtualFile> list = new ArrayList<VirtualFile>(classes.length);
263         for (PsiClass aClass : classes) {
264           final PsiFile psiFile = aClass.getContainingFile();
265           
266           if (LOG.isDebugEnabled()) {
267             final StringBuilder msg = new StringBuilder();
268             msg.append("Checking class ").append(aClass.getQualifiedName());
269             msg.append("\n\t").append("PsiFile=").append(psiFile);
270             if (psiFile != null) {
271               final VirtualFile vFile = psiFile.getVirtualFile();
272               msg.append("\n\t").append("VirtualFile=").append(vFile);
273               if (vFile != null) {
274                 msg.append("\n\t").append("isInSourceContent=").append(fileIndex.isInSourceContent(vFile));
275               }
276             }
277             LOG.debug(msg.toString());
278           }
279           
280           if (psiFile == null) {
281             return null;
282           }
283           final VirtualFile vFile = psiFile.getVirtualFile();
284           if (vFile == null || !fileIndex.isInSourceContent(vFile)) {
285             return null; // this will switch off the check if at least one class is from libraries
286           }
287           list.add(vFile);
288         }
289         return list;
290       }
291     });
292   }
293
294   public boolean evaluateCondition(EvaluationContextImpl context, LocatableEvent event) throws EvaluateException {
295     if(CLASS_FILTERS_ENABLED){
296       String className = null;
297       final ObjectReference thisObject = (ObjectReference)context.getThisObject();
298       if(thisObject != null) {
299         className = thisObject.referenceType().name();
300       }
301       else {
302         final StackFrameProxyImpl frame = context.getFrameProxy();
303         if (frame != null) {
304           className = frame.location().declaringType().name();
305         }
306       }
307       if (className != null) {
308         boolean matches = false;
309         for (ClassFilter classFilter : getClassFilters()) {
310           if (classFilter.isEnabled() && classFilter.matches(className)) {
311             matches = true;
312             break;
313           }
314         }
315         if(!matches) {
316           return false;
317         }
318         for (ClassFilter classFilter : getClassExclusionFilters()) {
319           if (classFilter.isEnabled() && classFilter.matches(className)) {
320             return false;
321           }
322         }
323       }
324     }
325     return super.evaluateCondition(context, event);
326   }
327
328   public String toString() {
329     return getDescription();
330   }
331
332   @Override
333   public String getShortName() {
334     return getDisplayInfoInternal(false, 30);
335   }
336
337   public String getDisplayName() {
338     return getDisplayInfoInternal(true, -1);
339   }
340
341   private String getDisplayInfoInternal(boolean showPackageInfo, int totalTextLength) {
342     final RangeHighlighter highlighter = getHighlighter();
343     if(highlighter.isValid() && isValid()) {
344       final int lineNumber = (highlighter.getDocument().getLineNumber(highlighter.getStartOffset()) + 1);
345       String className = getClassName();
346       final boolean hasClassInfo = className != null && className.length() > 0;
347       final boolean hasMethodInfo = myMethodName != null && myMethodName.length() > 0;
348       if (hasClassInfo || hasMethodInfo) {
349         final StringBuilder info = StringBuilderSpinAllocator.alloc();
350         try {
351           boolean isFile = getSourcePosition().getFile().getName().equals(className);
352           String packageName = null;
353           if (hasClassInfo) {
354             final int dotIndex = className.lastIndexOf(".");
355             if (dotIndex >= 0 && !isFile) {
356               packageName = className.substring(0, dotIndex);
357               className = className.substring(dotIndex + 1); 
358             }
359
360             if (totalTextLength != -1) {
361               if (className.length() + (hasMethodInfo ? myMethodName.length() : 0) > totalTextLength + 3) {
362                 int offset = totalTextLength - (hasMethodInfo ? myMethodName.length() : 0);
363                 if (offset > 0 && offset < className.length()) {
364                   className = className.substring(className.length() - offset);
365                   info.append("...");
366                 }
367               }
368             }
369             
370             info.append(className);
371           }
372           if(hasMethodInfo) {
373             if (isFile) {
374               info.append(":");
375             }
376             else if (hasClassInfo) {
377               info.append(".");
378             }
379             info.append(myMethodName);
380           }
381           if (showPackageInfo && packageName != null) {
382             info.append(" (").append(packageName).append(")");
383           }
384           return DebuggerBundle.message("line.breakpoint.display.name.with.class.or.method", lineNumber, info.toString());
385         }
386         finally {
387           StringBuilderSpinAllocator.dispose(info);
388         }
389       }
390       return DebuggerBundle.message("line.breakpoint.display.name", lineNumber);
391     }
392     return DebuggerBundle.message("status.breakpoint.invalid");
393   }
394
395   private static @Nullable String findMethodName(final PsiFile file, final int offset) {
396     if (file instanceof JspFile) {
397       return null;
398     }
399     if (file instanceof PsiClassOwner) {
400       return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
401         public String compute() {
402           final PsiMethod method = DebuggerUtilsEx.findPsiMethod(file, offset);
403           return method != null? method.getName() + "()" : null;
404         }
405       });
406     }
407     return null;
408   }
409
410   public String getEventMessage(LocatableEvent event) {
411     final Location location = event.location();
412     String sourceName = "Unknown Source";
413     try {
414       sourceName = location.sourceName();
415     }
416     catch (AbsentInformationException e) {
417       sourceName = getSourcePosition().getFile().getName();
418     }
419
420     final boolean printFullTrace = Registry.is("debugger.breakpoint.message.full.trace");
421
422     StringBuilder builder = new StringBuilder();
423     if (printFullTrace) {
424       builder.append(DebuggerBundle.message(
425         "status.line.breakpoint.reached.full.trace",
426         location.declaringType().name() + "." + location.method().name())
427       );
428       try {
429         final List<StackFrame> frames = event.thread().frames();
430         renderTrace(frames, builder);
431       }
432       catch (IncompatibleThreadStateException e) {
433         builder.append("Stacktrace not available: ").append(e.getMessage());
434       }
435     }
436     else {
437       builder.append(DebuggerBundle.message(
438         "status.line.breakpoint.reached",
439         location.declaringType().name() + "." + location.method().name(),
440         sourceName,
441         getLineIndex() + 1
442       ));
443     }
444     return builder.toString();
445   }
446
447   private static void renderTrace(List<StackFrame> frames, StringBuilder buffer) {
448     for (final StackFrame stackFrame : frames) {
449       final Location location = stackFrame.location();
450       buffer.append("\n\t  ").append(ThreadDumpAction.renderLocation(location));
451     }
452   }
453
454   public PsiElement getEvaluationElement() {
455     return PositionUtil.getContextElement(getSourcePosition());
456   }
457
458   protected static LineBreakpoint create(Project project, Document document, int lineIndex) {
459     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
460     if (virtualFile == null) {
461       return null;
462     }
463
464     final RangeHighlighter highlighter = createHighlighter(project, document, lineIndex);
465     if (highlighter == null) {
466       return null;
467     }
468
469     LineBreakpoint breakpoint = new LineBreakpoint(project, highlighter);
470     return (LineBreakpoint)breakpoint.init();
471   }
472
473   public boolean canMoveTo(SourcePosition position) {
474     if (!super.canMoveTo(position)) {
475       return false;
476     }
477     final Document document = PsiDocumentManager.getInstance(getProject()).getDocument(position.getFile());
478     return canAddLineBreakpoint(myProject, document, position.getLine());
479   }
480
481   public static boolean canAddLineBreakpoint(Project project, final Document document, final int lineIndex) {
482     if (lineIndex < 0 || lineIndex >= document.getLineCount()) {
483       return false;
484     }
485     final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(project).getBreakpointManager();
486     final LineBreakpoint breakpointAtLine = breakpointManager.findBreakpoint( document, document.getLineStartOffset(lineIndex), CATEGORY);
487     if (breakpointAtLine != null) {
488       // there already exists a line breakpoint at this line
489       return false;
490     }
491     PsiDocumentManager.getInstance(project).commitDocument(document);
492
493     final boolean[] canAdd = new boolean[]{false};
494     XDebuggerUtil.getInstance().iterateLine(project, document, lineIndex, new Processor<PsiElement>() {
495       public boolean process(PsiElement element) {
496         if ((element instanceof PsiWhiteSpace) || (PsiTreeUtil.getParentOfType(element, PsiComment.class, false) != null)) {
497           return true;
498         }
499         PsiElement child = element;
500         while(element != null) {
501
502           final int offset = element.getTextOffset();
503           if (offset >= 0) {
504             if (document.getLineNumber(offset) != lineIndex) {
505               break;
506             }
507           }
508           child = element;
509           element = element.getParent();
510         }
511
512         if(child instanceof PsiMethod && child.getTextRange().getEndOffset() >= document.getLineEndOffset(lineIndex)) {
513           PsiCodeBlock body = ((PsiMethod)child).getBody();
514           if(body == null) {
515             canAdd[0] = false;
516           }
517           else {
518             PsiStatement[] statements = body.getStatements();
519             canAdd[0] = statements.length > 0 && document.getLineNumber(statements[0].getTextOffset()) == lineIndex;
520           }
521         }
522         else {
523           canAdd[0] = true;
524         }
525         return false;
526       }
527     });
528
529     return canAdd[0];
530   }
531
532   public @Nullable String getMethodName() {
533     return myMethodName;
534   }
535 }