0499332ee732bd15410243138d0f5a100aaebe19
[idea/community.git] / plugins / groovy / groovy-psi / src / org / jetbrains / plugins / groovy / dsl / GroovyDslFileIndex.java
1 /*
2  * Copyright 2000-2014 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 package org.jetbrains.plugins.groovy.dsl;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.application.ex.ApplicationUtil;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
22 import com.intellij.openapi.progress.EmptyProgressIndicator;
23 import com.intellij.openapi.progress.ProcessCanceledException;
24 import com.intellij.openapi.progress.ProgressManager;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.project.ProjectManager;
27 import com.intellij.openapi.roots.ProjectFileIndex;
28 import com.intellij.openapi.roots.ProjectRootManager;
29 import com.intellij.openapi.util.Key;
30 import com.intellij.openapi.util.ModificationTracker;
31 import com.intellij.openapi.util.Pair;
32 import com.intellij.openapi.util.Trinity;
33 import com.intellij.openapi.util.io.FileUtil;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.openapi.vfs.VirtualFile;
36 import com.intellij.openapi.vfs.VirtualFileAdapter;
37 import com.intellij.openapi.vfs.VirtualFileEvent;
38 import com.intellij.openapi.vfs.VirtualFileManager;
39 import com.intellij.psi.*;
40 import com.intellij.psi.impl.PsiModificationTrackerImpl;
41 import com.intellij.psi.scope.DelegatingScopeProcessor;
42 import com.intellij.psi.scope.PsiScopeProcessor;
43 import com.intellij.psi.search.GlobalSearchScope;
44 import com.intellij.psi.util.CachedValue;
45 import com.intellij.psi.util.CachedValueProvider;
46 import com.intellij.psi.util.CachedValuesManager;
47 import com.intellij.psi.util.PsiModificationTracker;
48 import com.intellij.reference.SoftReference;
49 import com.intellij.util.ConcurrencyUtil;
50 import com.intellij.util.ExceptionUtil;
51 import com.intellij.util.Function;
52 import com.intellij.util.PathUtil;
53 import com.intellij.util.containers.ConcurrentMultiMap;
54 import com.intellij.util.containers.ContainerUtil;
55 import com.intellij.util.containers.MultiMap;
56 import com.intellij.util.indexing.*;
57 import com.intellij.util.io.EnumeratorStringDescriptor;
58 import com.intellij.util.io.KeyDescriptor;
59 import org.jetbrains.annotations.NonNls;
60 import org.jetbrains.annotations.NotNull;
61 import org.jetbrains.annotations.Nullable;
62 import org.jetbrains.plugins.groovy.GroovyFileType;
63 import org.jetbrains.plugins.groovy.annotator.GroovyFrameworkConfigNotification;
64 import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
65 import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
66 import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
67 import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil;
68 import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
69
70 import java.io.File;
71 import java.io.IOException;
72 import java.util.*;
73 import java.util.concurrent.Callable;
74 import java.util.concurrent.LinkedBlockingQueue;
75 import java.util.concurrent.ThreadPoolExecutor;
76 import java.util.concurrent.TimeUnit;
77 import java.util.regex.Pattern;
78
79 /**
80  * @author peter
81  */
82 public class GroovyDslFileIndex extends ScalarIndexExtension<String> {
83   private static final Key<Pair<GroovyDslExecutor, Long>> CACHED_EXECUTOR = Key.create("CachedGdslExecutor");
84   private static final Logger LOG = Logger.getInstance(GroovyDslFileIndex.class);
85
86   @NonNls public static final ID<String, Void> NAME = ID.create("GroovyDslFileIndex");
87   @NonNls private static final String OUR_KEY = "ourKey";
88   public static final String MODIFIED = "Modified";
89   private final MyDataIndexer myDataIndexer = new MyDataIndexer();
90
91   private static final MultiMap<String, LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>>> filesInProcessing =
92     new ConcurrentMultiMap<String, LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>>>();
93
94   private static final ThreadPoolExecutor ourPool = new ThreadPoolExecutor(0, 4, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), ConcurrencyUtil.newNamedThreadFactory("Groovy DSL File Index Executor"));
95
96   private final EnumeratorStringDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
97
98   public GroovyDslFileIndex() {
99     VirtualFileManager.getInstance().addVirtualFileListener(new VirtualFileAdapter() {
100
101       @Override
102       public void contentsChanged(@NotNull VirtualFileEvent event) {
103         if (event.getFileName().endsWith(".gdsl")) {
104           disableFile(event.getFile(), MODIFIED);
105         }
106       }
107
108     });
109   }
110
111   @Override
112   @NotNull
113   public ID<String, Void> getName() {
114     return NAME;
115   }
116
117   @Override
118   @NotNull
119   public DataIndexer<String, Void, FileContent> getIndexer() {
120     return myDataIndexer;
121   }
122
123   @NotNull
124   @Override
125   public KeyDescriptor<String> getKeyDescriptor() {
126     return myKeyDescriptor;
127   }
128
129   @NotNull
130   @Override
131   public FileBasedIndex.InputFilter getInputFilter() {
132     return new MyInputFilter();
133   }
134
135   @Override
136   public boolean dependsOnFileContent() {
137     return false;
138   }
139
140   @Override
141   public int getVersion() {
142     return 0;
143   }
144
145   @Nullable
146   public static String getInactivityReason(VirtualFile file) {
147     return DslActivationStatus.getInstance().getInactivityReason(file);
148   }
149
150   public static boolean isActivated(VirtualFile file) {
151     return DslActivationStatus.getInstance().isActivated(file);
152   }
153
154   public static void activateUntilModification(final VirtualFile vfile) {
155     DslActivationStatus.getInstance().activateUntilModification(vfile);
156     clearScriptCache();
157   }
158
159   private static void clearScriptCache() {
160     for (Project project : ProjectManager.getInstance().getOpenProjects()) {
161       project.putUserData(SCRIPTS_CACHE, null);
162       ((PsiModificationTrackerImpl)PsiManager.getInstance(project).getModificationTracker()).incCounter();
163     }
164   }
165
166   static void disableFile(final VirtualFile vfile, String error) {
167     DslActivationStatus.getInstance().disableFile(vfile, error);
168     vfile.putUserData(CACHED_EXECUTOR, null);
169     clearScriptCache();
170   }
171
172
173   @Nullable
174   private static GroovyDslExecutor getCachedExecutor(@NotNull final VirtualFile file, final long stamp) {
175     final Pair<GroovyDslExecutor, Long> pair = file.getUserData(CACHED_EXECUTOR);
176     if (pair == null || pair.second.longValue() != stamp) {
177       return null;
178     }
179     return pair.first;
180   }
181
182   @Nullable
183   public static PsiClassType pocessScriptSuperClasses(@NotNull GroovyFile scriptFile) {
184     if (!scriptFile.isScript()) return null;
185
186     final VirtualFile virtualFile = scriptFile.getVirtualFile();
187     if (virtualFile == null) return null;
188     final String filePath = virtualFile.getPath();
189
190
191     List<Trinity<String, String, GroovyDslScript>> supers = ContainerUtil.newArrayList();
192     final Project project = scriptFile.getProject();
193     for (GroovyDslScript script : getDslScripts(project)) {
194       final MultiMap staticInfo = script.getStaticInfo();
195       final Collection infos = staticInfo != null ? staticInfo.get("scriptSuperClass") : Collections.emptyList();
196
197       for (Object info : infos) {
198         if (info instanceof Map) {
199           final Map map = (Map)info;
200
201           final Object _pattern = map.get("pattern");
202           final Object _superClass = map.get("superClass");
203
204           if (_pattern instanceof String && _superClass instanceof String) {
205             final String pattern = (String)_pattern;
206             final String superClass = (String)_superClass;
207
208             try {
209               if (Pattern.matches(".*" + pattern, filePath)) {
210                 supers.add(Trinity.create(superClass, pattern, script));
211               }
212             }
213             catch (RuntimeException e) {
214               script.handleDslError(e);
215             }
216           }
217         }
218       }
219     }
220
221     if (!supers.isEmpty()) {
222       final String className = supers.get(0).first;
223       final GroovyDslScript script = supers.get(0).third;
224       try {
225         return TypesUtil.createTypeByFQClassName(className, scriptFile);
226       }
227       catch (ProcessCanceledException e) {
228         throw e;
229       }
230       catch (RuntimeException e) {
231         script.handleDslError(e);
232         return null;
233       }
234     }
235     /*else if (supers.size() > 1) {
236       StringBuilder buffer = new StringBuilder("Several script super class patterns match file ").append(filePath).append(". <p> ");
237       for (Trinity<String, String, GroovyDslScript> aSuper : supers) {
238         buffer.append(aSuper.third.getFilePath()).append(" ").append(aSuper.second).append('\n');
239       }
240       NOTIFICATION_GROUP.createNotification("DSL script execution error", buffer.toString(), NotificationType.ERROR, null).notify(project);
241       return null;
242     }*/
243     else {
244       return null;
245     }
246   }
247
248   public static boolean processExecutors(PsiType psiType, PsiElement place, final PsiScopeProcessor processor, ResolveState state) {
249     if (insideAnnotation(place)) {
250       // Basic filter, all DSL contexts are applicable for reference expressions only
251       return true;
252     }
253
254     final String qname = psiType.getCanonicalText();
255
256     final PsiFile placeFile = place.getContainingFile().getOriginalFile();
257
258     final DelegatingScopeProcessor nameChecker = new DelegatingScopeProcessor(processor) {
259       @Override
260       public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
261         if (element instanceof PsiMethod && ((PsiMethod)element).isConstructor()) {
262           return processor.execute(element, state);
263         }
264         else if (element instanceof PsiNamedElement) {
265           return ResolveUtil.processElement(processor, (PsiNamedElement)element, state);
266         }
267         else {
268           return processor.execute(element, state);
269         }
270       }
271     };
272
273     for (GroovyDslScript script : getDslScripts(place.getProject())) {
274       if (!script.processExecutor(nameChecker, psiType, place, placeFile, qname, state)) {
275         return false;
276       }
277     }
278
279     return true;
280   }
281
282   private static boolean insideAnnotation(@Nullable PsiElement place) {
283     while (place != null) {
284       if (place instanceof PsiAnnotation) return true;
285       if (place instanceof GrClosableBlock ||
286           place instanceof GrTypeDefinition ||
287           place instanceof PsiFile) return false;
288       place = place.getParent();
289     }
290     return false;
291   }
292
293   private static volatile SoftReference<List<Pair<File, GroovyDslExecutor>>> ourStandardScripts;
294
295   @Nullable
296   private static List<Pair<File, GroovyDslExecutor>> derefStandardScripts() {
297     return SoftReference.dereference(ourStandardScripts);
298   }
299
300   @Nullable
301   private static List<Pair<File, GroovyDslExecutor>> getStandardScripts() {
302     List<Pair<File, GroovyDslExecutor>> result = derefStandardScripts();
303     if (result != null) {
304       return result;
305     }
306
307     final GroovyFrameworkConfigNotification[] extensions = GroovyFrameworkConfigNotification.EP_NAME.getExtensions();
308     Callable<List<Pair<File, GroovyDslExecutor>>> action = new Callable<List<Pair<File, GroovyDslExecutor>>>() {
309       @Override
310       public List<Pair<File, GroovyDslExecutor>> call() throws Exception {
311         if (GdslUtil.ourGdslStopped) {
312           return null;
313         }
314
315         try {
316           List<Pair<File, GroovyDslExecutor>> pairs = derefStandardScripts();
317           if (pairs != null) {
318             return pairs;
319           }
320
321           Set<Class> classes = new HashSet<Class>(ContainerUtil.map2Set(extensions, new Function<GroovyFrameworkConfigNotification, Class>() {
322             @Override
323             public Class fun(GroovyFrameworkConfigNotification notification) {
324               return notification.getClass();
325             }
326           }));
327           classes.add(GroovyFrameworkConfigNotification.class); // for default extension
328
329           // perhaps a separate extension for that?
330           Set<File> scriptFolders = new LinkedHashSet<File>();
331           for (Class aClass : classes) {
332             File jarPath = new File(PathUtil.getJarPathForClass(aClass));
333             if (jarPath.isFile()) {
334               jarPath = jarPath.getParentFile();
335             }
336             scriptFolders.add(new File(jarPath, "standardDsls"));
337           }
338
339           List<Pair<File, GroovyDslExecutor>> executors = new ArrayList<Pair<File, GroovyDslExecutor>>();
340           for (File file : scriptFolders) {
341             if (file.exists()) {
342               File[] children = file.listFiles();
343               if (children != null) {
344                 for (File child : children) {
345                   final String fileName = child.getName();
346                   if (fileName.endsWith(".gdsl")) {
347                     try {
348                       final String text = new String(FileUtil.loadFileText(child));
349                       executors.add(Pair.create(child, new GroovyDslExecutor(text, fileName)));
350                     }
351                     catch (IOException e) {
352                       LOG.error("Error while parsing gdsl file " + fileName, e);
353                     }
354                   }
355                 }
356               }
357             }
358           }
359           //noinspection AssignmentToStaticFieldFromInstanceMethod
360           ourStandardScripts = new SoftReference<List<Pair<File, GroovyDslExecutor>>>(executors);
361           return executors;
362         }
363         catch (Throwable e) {
364           //noinspection InstanceofCatchParameter
365           if (e instanceof Error) {
366             GdslUtil.stopGdsl();
367           }
368           LOG.error(e);
369           return null;
370         }
371       }
372     };
373
374     try {
375       if (ApplicationManager.getApplication().isDispatchThread()) {
376         return action.call();
377       }
378       return ApplicationUtil.runWithCheckCanceled(action, new EmptyProgressIndicator(), ourPool);
379     }
380     catch (Exception e) {
381       ExceptionUtil.rethrowUnchecked(e);
382       LOG.error(e);
383       return null;
384     }
385   }
386
387   private static final Key<CachedValue<List<GroovyDslScript>>> SCRIPTS_CACHE = Key.create("GdslScriptCache");
388
389   private static List<GroovyDslScript> getDslScripts(final Project project) {
390     return CachedValuesManager.getManager(project).getCachedValue(project, SCRIPTS_CACHE, new CachedValueProvider<List<GroovyDslScript>>() {
391       @Override
392       public Result<List<GroovyDslScript>> compute() {
393         if (GdslUtil.ourGdslStopped) {
394           return Result.create(Collections.<GroovyDslScript>emptyList(), ModificationTracker.NEVER_CHANGED);
395         }
396
397         // eagerly initialize some services used by background gdsl parsing threads
398         // because service init requires a read action
399         // and there could be a deadlock with a write action waiting already on EDT
400         // if current thread is inside a non-cancellable read action
401         GroovyDslExecutor.getIdeaVersion();
402         DslActivationStatus.getInstance();
403
404         int count = 0;
405
406         List<GroovyDslScript> result = new ArrayList<GroovyDslScript>();
407
408         List<Pair<File, GroovyDslExecutor>> standardScripts = getStandardScripts();
409         if (standardScripts != null) {
410           for (Pair<File, GroovyDslExecutor> pair : standardScripts) {
411             result.add(new GroovyDslScript(project, null, pair.second, pair.first.getPath()));
412           }
413         }
414
415         final LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>> queue =
416           new LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>>();
417
418         final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
419         final GlobalSearchScope scope = GlobalSearchScope.allScope(project);
420         for (VirtualFile vfile : FileBasedIndex.getInstance().getContainingFiles(NAME, OUR_KEY, scope)) {
421           if (!vfile.isValid()) {
422             continue;
423           }
424           if (fileIndex.isInLibrarySource(vfile)) {
425             continue;
426           }
427           if (!fileIndex.isInLibraryClasses(vfile)) {
428             if (!fileIndex.isInSourceContent(vfile) || !isActivated(vfile)) {
429               continue;
430             }
431           }
432
433           final long stamp = vfile.getModificationStamp();
434           final GroovyDslExecutor cached = getCachedExecutor(vfile, stamp);
435           if (cached == null) {
436             scheduleParsing(queue, project, vfile, stamp, LoadTextUtil.loadText(vfile).toString());
437             count++;
438           }
439           else {
440             result.add(new GroovyDslScript(project, vfile, cached, vfile.getPath()));
441           }
442         }
443
444         try {
445           while (count > 0 && !GdslUtil.ourGdslStopped) {
446             ProgressManager.checkCanceled();
447             final Pair<VirtualFile, GroovyDslExecutor> pair = queue.poll(20, TimeUnit.MILLISECONDS);
448             if (pair != null) {
449               count--;
450               if (pair.second != null) {
451                 result.add(new GroovyDslScript(project, pair.first, pair.second, pair.first.getPath()));
452               }
453             }
454           }
455         }
456         catch (InterruptedException e) {
457           LOG.error(e);
458         }
459
460         return Result.create(result, PsiModificationTracker.MODIFICATION_COUNT, ProjectRootManager.getInstance(project));
461       }
462     }, false);
463   }
464
465   private static class MyDataIndexer implements DataIndexer<String, Void, FileContent> {
466
467     @Override
468     @NotNull
469     public Map<String, Void> map(@NotNull final FileContent inputData) {
470       return Collections.singletonMap(OUR_KEY, null);
471     }
472   }
473
474   private static class MyInputFilter extends DefaultFileTypeSpecificInputFilter {
475     MyInputFilter() {
476       super(GroovyFileType.GROOVY_FILE_TYPE);
477     }
478
479     @Override
480     public boolean acceptInput(@NotNull final VirtualFile file) {
481       return StringUtil.endsWith(file.getNameSequence(), ".gdsl");
482     }
483   }
484
485   private static void scheduleParsing(final LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>> queue,
486                                       final Project project,
487                                       final VirtualFile vfile,
488                                       final long stamp,
489                                       final String text) {
490     final String fileUrl = vfile.getUrl();
491
492     final Runnable parseScript = new Runnable() {
493       @Override
494       public void run() {
495         GroovyDslExecutor executor = getCachedExecutor(vfile, stamp);
496         try {
497           if (executor == null && isActivated(vfile)) {
498             executor = createExecutor(text, vfile, project);
499             // executor is not only time-consuming to create, but also takes some PermGenSpace
500             // => we can't afford garbage-collecting it together with PsiFile
501             // => cache globally by file instance
502             vfile.putUserData(CACHED_EXECUTOR, Pair.create(executor, stamp));
503             if (executor != null) {
504               activateUntilModification(vfile);
505             }
506           }
507         }
508         finally {
509           // access to our MultiMap should be synchronized
510           synchronized (filesInProcessing) {
511             // put evaluated executor to all queues
512             for (LinkedBlockingQueue<Pair<VirtualFile, GroovyDslExecutor>> queue : filesInProcessing.remove(fileUrl)) {
513               queue.offer(Pair.create(vfile, executor));
514             }
515           }
516         }
517       }
518     };
519
520     //noinspection SynchronizationOnLocalVariableOrMethodParameter
521     synchronized (filesInProcessing) { //ensure that only one thread calculates dsl executor
522       final boolean isNewRequest = !filesInProcessing.containsKey(fileUrl);
523       filesInProcessing.putValue(fileUrl, queue);
524       if (isNewRequest) {
525         ourPool.execute(parseScript);
526       }
527     }
528   }
529
530   @Nullable
531   private static GroovyDslExecutor createExecutor(String text, VirtualFile vfile, final Project project) {
532     if (GdslUtil.ourGdslStopped) {
533       return null;
534     }
535
536     try {
537       return new GroovyDslExecutor(text, vfile.getName());
538     }
539     catch (final Throwable e) {
540       if (project.isDisposed()) {
541         LOG.error(e);
542         return null;
543       }
544
545       if (ApplicationManager.getApplication().isUnitTestMode()) {
546         LOG.error(e);
547         return null;
548       }
549       invokeDslErrorPopup(e, project, vfile);
550
551       //noinspection InstanceofCatchParameter
552       if (e instanceof OutOfMemoryError) {
553         GdslUtil.stopGdsl();
554         throw (Error)e;
555       }
556       //noinspection InstanceofCatchParameter
557       if (e instanceof NoClassDefFoundError) {
558         GdslUtil.stopGdsl();
559         throw (NoClassDefFoundError) e;
560       }
561
562       return null;
563     }
564   }
565   static void invokeDslErrorPopup(Throwable e, final Project project, @NotNull VirtualFile vfile) {
566     DslErrorReporter.getInstance().invokeDslErrorPopup(e, project, vfile);
567   }
568
569 }