FUS-450 add project as parameter to FileTypeUsageSchemaDescriptor
[idea/community.git] / platform / platform-impl / src / com / intellij / internal / statistic / collectors / fus / fileTypes / FileTypeUsageCounterCollector.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.internal.statistic.collectors.fus.fileTypes;
3
4 import com.intellij.internal.statistic.eventLog.*;
5 import com.intellij.internal.statistic.eventLog.fus.FeatureUsageLogger;
6 import com.intellij.internal.statistic.eventLog.validator.ValidationResultType;
7 import com.intellij.internal.statistic.eventLog.validator.rules.EventContext;
8 import com.intellij.internal.statistic.eventLog.validator.rules.impl.CustomValidationRule;
9 import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector;
10 import com.intellij.openapi.actionSystem.AnAction;
11 import com.intellij.openapi.actionSystem.AnActionEvent;
12 import com.intellij.openapi.actionSystem.CommonDataKeys;
13 import com.intellij.openapi.actionSystem.DataContext;
14 import com.intellij.openapi.actionSystem.ex.AnActionListener;
15 import com.intellij.openapi.diagnostic.Logger;
16 import com.intellij.openapi.editor.Editor;
17 import com.intellij.openapi.editor.actionSystem.EditorAction;
18 import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler;
19 import com.intellij.openapi.extensions.ExtensionPointName;
20 import com.intellij.openapi.fileEditor.FileDocumentManager;
21 import com.intellij.openapi.fileEditor.FileEditorManager;
22 import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
23 import com.intellij.openapi.fileEditor.FileEditorManagerListener;
24 import com.intellij.openapi.fileTypes.FileType;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.Key;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.serviceContainer.BaseKeyedLazyInstance;
30 import com.intellij.util.ArrayUtil;
31 import com.intellij.util.KeyedLazyInstance;
32 import com.intellij.util.xmlb.annotations.Attribute;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import static com.intellij.internal.statistic.utils.PluginInfoDetectorKt.getPluginInfo;
37
38 public class FileTypeUsageCounterCollector extends CounterUsagesCollector {
39   private static final Logger LOG = Logger.getInstance(FileTypeUsageCounterCollector.class);
40
41   private static final ExtensionPointName<FileTypeUsageSchemaDescriptorEP<FileTypeUsageSchemaDescriptor>> EP =
42     ExtensionPointName.create("com.intellij.fileTypeUsageSchemaDescriptor");
43
44   private static final EventLogGroup GROUP = new EventLogGroup("file.types.usage", FeatureUsageLogger.INSTANCE.getConfig().getVersion());
45
46   private static final EventField<String> FILE_TYPE = EventFields.String("file_type").withCustomRule("file_type");
47   private static final EventField<String> SCHEMA = EventFields.String("schema").withCustomRule("file_type_schema");
48   private static final EventField<Boolean> IS_WRITABLE = EventFields.Boolean("is_writable");
49
50   @Override
51   public EventLogGroup getGroup() {
52     return GROUP;
53   }
54
55   private static VarargEventId registerFileTypeEvent(String eventId, EventField<?> ... extraFields) {
56     EventField<?>[] baseFields = {EventFields.PluginInfoFromInstance, FILE_TYPE, EventFields.AnonymizedPath, SCHEMA};
57     return GROUP.registerVarargEvent(eventId, ArrayUtil.mergeArrays(baseFields, extraFields));
58   }
59
60   private static final VarargEventId SELECT = registerFileTypeEvent("select");
61   private static final VarargEventId EDIT = registerFileTypeEvent("edit");
62   private static final VarargEventId OPEN = registerFileTypeEvent("open", IS_WRITABLE);
63   private static final VarargEventId CLOSE = registerFileTypeEvent("close", IS_WRITABLE);
64
65   public static void triggerEdit(@NotNull Project project, @NotNull VirtualFile file) {
66     log(EDIT, project, file);
67   }
68
69   public static void triggerSelect(@NotNull Project project, @Nullable VirtualFile file) {
70     if (file != null) {
71       log(SELECT, project, file);
72     }
73     else {
74       logEmptyFile();
75     }
76   }
77
78   public static void triggerOpen(@NotNull Project project, @NotNull VirtualFile file) {
79     OPEN.log(project, ArrayUtil.append(buildCommonEventPairs(project, file), IS_WRITABLE.with(file.isWritable())));
80   }
81
82   public static void triggerClosed(@NotNull Project project, @NotNull VirtualFile file) {
83     CLOSE.log(project, ArrayUtil.append(buildCommonEventPairs(project, file), IS_WRITABLE.with(file.isWritable())));
84   }
85
86   private static void log(@NotNull VarargEventId eventId, @NotNull Project project, @NotNull VirtualFile file) {
87     eventId.log(project, buildCommonEventPairs(project, file));
88   }
89
90   private static EventPair<?> @NotNull [] buildCommonEventPairs(@NotNull Project project,
91                                                                 @NotNull VirtualFile file) {
92     FileType fileType = file.getFileType();
93     return new EventPair[]{EventFields.PluginInfoFromInstance.with(fileType),
94       FILE_TYPE.with(FileTypeUsagesCollector.getSafeFileTypeName(fileType)),
95       EventFields.AnonymizedPath.with(file.getPath()),
96       SCHEMA.with(findSchema(project, file))};
97   }
98
99   private static void logEmptyFile() {
100     SELECT.log(EventFields.AnonymizedPath.with(null));
101   }
102
103   public static @Nullable String findSchema(@NotNull Project project,
104                                             @NotNull VirtualFile file) {
105     for (FileTypeUsageSchemaDescriptorEP<FileTypeUsageSchemaDescriptor> ext : EP.getExtensionList()) {
106       FileTypeUsageSchemaDescriptor instance = ext.getInstance();
107       if (ext.schema == null) {
108         LOG.warn("Extension " + ext.implementationClass + " should define a 'schema' attribute");
109         continue;
110       }
111
112       if (instance.describes(project, file)) {
113         return getPluginInfo(instance.getClass()).isSafeToReport() ? ext.schema : "third.party";
114       }
115     }
116     return null;
117   }
118
119   public static final class FileTypeUsageSchemaDescriptorEP<T> extends BaseKeyedLazyInstance<T> implements KeyedLazyInstance<T> {
120     // these must be public for scrambling compatibility
121     @Attribute("schema")
122     public String schema;
123
124     @Attribute("implementationClass")
125     public String implementationClass;
126
127     @Nullable
128     @Override
129     protected String getImplementationClassName() {
130       return implementationClass;
131     }
132
133     @Override
134     public String getKey() {
135       return schema;
136     }
137   }
138
139   public static final class FileTypeSchemaValidator extends CustomValidationRule {
140
141     @Override
142     public boolean acceptRuleId(@Nullable String ruleId) {
143       return "file_type_schema".equals(ruleId);
144     }
145
146     @NotNull
147     @Override
148     protected ValidationResultType doValidate(@NotNull String data, @NotNull EventContext context) {
149       if (isThirdPartyValue(data)) return ValidationResultType.ACCEPTED;
150
151       for (FileTypeUsageSchemaDescriptorEP<FileTypeUsageSchemaDescriptor> ext : EP.getExtensionList()) {
152         if (StringUtil.equals(ext.schema, data)) {
153           return getPluginInfo(ext.getInstance().getClass()).isSafeToReport() ?
154                  ValidationResultType.ACCEPTED : ValidationResultType.THIRD_PARTY;
155         }
156       }
157       return ValidationResultType.REJECTED;
158     }
159   }
160
161   public static class MyAnActionListener implements AnActionListener {
162     private static final Key<Long> LAST_EDIT_USAGE = Key.create("LAST_EDIT_USAGE");
163
164     @Override
165     public void beforeActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
166       if (action instanceof EditorAction && ((EditorAction)action).getHandlerOfType(EditorWriteActionHandler.class) != null) {
167         onChange(dataContext);
168       }
169     }
170
171     private static void onChange(DataContext dataContext) {
172       final Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
173       if (editor == null) return;
174       Project project = editor.getProject();
175       if (project == null) return;
176       VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
177       if (file != null) {
178         Long lastEdit = editor.getUserData(LAST_EDIT_USAGE);
179         if (lastEdit == null || System.currentTimeMillis() - lastEdit > 60 * 1000) {
180           editor.putUserData(LAST_EDIT_USAGE, System.currentTimeMillis());
181           triggerEdit(project, file);
182         }
183       }
184     }
185
186     @Override
187     public void beforeEditorTyping(char c, @NotNull DataContext dataContext) {
188       onChange(dataContext);
189     }
190   }
191
192   public static class MyFileEditorManagerListener implements FileEditorManagerListener {
193     @Override
194     public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
195       triggerOpen(source.getProject(), file);
196     }
197
198     @Override
199     public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
200       triggerClosed(source.getProject(), file);
201     }
202
203     @Override
204     public void selectionChanged(@NotNull FileEditorManagerEvent event) {
205       triggerSelect(event.getManager().getProject(), event.getNewFile());
206     }
207   }
208 }