8ead46103e52ad7b9209ba22bdf933fe94fd023e
[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(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(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(file));
88   }
89
90   private static EventPair<?> @NotNull [] buildCommonEventPairs(@NotNull VirtualFile file) {
91     FileType fileType = file.getFileType();
92     return new EventPair[]{EventFields.PluginInfoFromInstance.with(fileType),
93       FILE_TYPE.with(FileTypeUsagesCollector.getSafeFileTypeName(fileType)),
94       EventFields.AnonymizedPath.with(file.getPath()),
95       SCHEMA.with(findSchema(file))};
96   }
97
98   private static void logEmptyFile() {
99     SELECT.log(EventFields.AnonymizedPath.with(null));
100   }
101
102   public static @Nullable String findSchema(@NotNull VirtualFile file) {
103     for (FileTypeUsageSchemaDescriptorEP<FileTypeUsageSchemaDescriptor> ext : EP.getExtensionList()) {
104       FileTypeUsageSchemaDescriptor instance = ext.getInstance();
105       if (ext.schema == null) {
106         LOG.warn("Extension " + ext.implementationClass + " should define a 'schema' attribute");
107         continue;
108       }
109
110       if(instance.describes(file)) {
111         return getPluginInfo(instance.getClass()).isSafeToReport() ? ext.schema : "third.party";
112       }
113     }
114     return null;
115   }
116
117   public static final class FileTypeUsageSchemaDescriptorEP<T> extends BaseKeyedLazyInstance<T> implements KeyedLazyInstance<T> {
118     // these must be public for scrambling compatibility
119     @Attribute("schema")
120     public String schema;
121
122     @Attribute("implementationClass")
123     public String implementationClass;
124
125     @Nullable
126     @Override
127     protected String getImplementationClassName() {
128       return implementationClass;
129     }
130
131     @Override
132     public String getKey() {
133       return schema;
134     }
135   }
136
137   public static final class FileTypeSchemaValidator extends CustomValidationRule {
138
139     @Override
140     public boolean acceptRuleId(@Nullable String ruleId) {
141       return "file_type_schema".equals(ruleId);
142     }
143
144     @NotNull
145     @Override
146     protected ValidationResultType doValidate(@NotNull String data, @NotNull EventContext context) {
147       if (isThirdPartyValue(data)) return ValidationResultType.ACCEPTED;
148
149       for (FileTypeUsageSchemaDescriptorEP<FileTypeUsageSchemaDescriptor> ext : EP.getExtensionList()) {
150         if (StringUtil.equals(ext.schema, data)) {
151           return getPluginInfo(ext.getInstance().getClass()).isSafeToReport() ?
152                  ValidationResultType.ACCEPTED : ValidationResultType.THIRD_PARTY;
153         }
154       }
155       return ValidationResultType.REJECTED;
156     }
157   }
158
159   public static class MyAnActionListener implements AnActionListener {
160     private static final Key<Long> LAST_EDIT_USAGE = Key.create("LAST_EDIT_USAGE");
161
162     @Override
163     public void beforeActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
164       if (action instanceof EditorAction && ((EditorAction)action).getHandlerOfType(EditorWriteActionHandler.class) != null) {
165         onChange(dataContext);
166       }
167     }
168
169     private static void onChange(DataContext dataContext) {
170       final Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
171       if (editor == null) return;
172       Project project = editor.getProject();
173       if (project == null) return;
174       VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
175       if (file != null) {
176         Long lastEdit = editor.getUserData(LAST_EDIT_USAGE);
177         if (lastEdit == null || System.currentTimeMillis() - lastEdit > 60 * 1000) {
178           editor.putUserData(LAST_EDIT_USAGE, System.currentTimeMillis());
179           triggerEdit(project, file);
180         }
181       }
182     }
183
184     @Override
185     public void beforeEditorTyping(char c, @NotNull DataContext dataContext) {
186       onChange(dataContext);
187     }
188   }
189
190   public static class MyFileEditorManagerListener implements FileEditorManagerListener {
191     @Override
192     public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
193       triggerOpen(source.getProject(), file);
194     }
195
196     @Override
197     public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
198       triggerClosed(source.getProject(), file);
199     }
200
201     @Override
202     public void selectionChanged(@NotNull FileEditorManagerEvent event) {
203       triggerSelect(event.getManager().getProject(), event.getNewFile());
204     }
205   }
206 }