javafx: Simplify resources for inspection of redundant FXML attributes and tags,...
[idea/community.git] / plugins / javaFX / javaFX-CE / testSrc / org / jetbrains / plugins / javaFX / fxml / JavaFxGenerateDefaultPropertyValuesScript.java
1 package org.jetbrains.plugins.javaFX.fxml;
2
3 import javafx.application.Application;
4 import javafx.application.Platform;
5 import javafx.geometry.NodeOrientation;
6 import javafx.geometry.Pos;
7 import javafx.scene.AccessibleRole;
8 import javafx.scene.Scene;
9 import javafx.scene.chart.LineChart;
10 import javafx.scene.control.Button;
11 import javafx.scene.layout.StackPane;
12 import javafx.stage.PopupWindow;
13 import javafx.stage.Stage;
14 import org.jetbrains.annotations.NotNull;
15
16 import java.beans.*;
17 import java.io.File;
18 import java.io.FileInputStream;
19 import java.io.IOException;
20 import java.lang.reflect.*;
21 import java.util.*;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24 import java.util.zip.ZipEntry;
25 import java.util.zip.ZipInputStream;
26
27 /**
28  * This is not a test, this is a resource generator for JavaFxRedundantPropertyValueInspection
29  * <p>
30  * When launched without arguments it produces default values for JavaFX classes having default constructor and their superclasses, including some (but not all) abstract classes
31  * <p>
32  * When launched with <code>-fromSource</code> argument it attempts to extract default property values from the sources (JavaDoc and declarations),
33  * the results can be used for updating the contents of {@link #ourFromSource} map, which contains manually edited properties
34  *
35  * @author Pavel.Dolgov
36  */
37 public class JavaFxGenerateDefaultPropertyValuesScript extends Application {
38   public static final String BINARIES_PATH = "/usr/lib/jvm/java-8-oracle/jre/lib/ext/jfxrt.jar";
39   public static final String SOURCE_PATH = "/usr/lib/jvm/java-8-oracle/javafx-src.zip";
40
41   public static void main(String[] args) {
42     if (args.length == 1 && "-fromSource".equals(args[0])) {
43       scanSource();
44     }
45     else {
46       launch(args);
47     }
48   }
49
50   @Override
51   public void start(Stage primaryStage) throws Exception {
52     Button b = new Button();
53     b.setText("Generate");
54     b.setOnAction(event -> generate());
55
56     StackPane pane = new StackPane();
57     pane.getChildren().add(b);
58     primaryStage.setScene(new Scene(pane, 300, 200));
59     primaryStage.show();
60
61     Platform.runLater(() -> {
62       generate();
63       primaryStage.close();
64     });
65   }
66
67   private static final Map<String, Object> ourFromSource = new TreeMap<>();
68
69   static {
70     ourFromSource.put("javafx.concurrent.ScheduledService#maximumFailureCount", Integer.MAX_VALUE);
71     ourFromSource.put("javafx.concurrent.ScheduledService#restartOnFailure", true);
72     ourFromSource.put("javafx.scene.Node#accessibleRole", AccessibleRole.NODE);
73     ourFromSource.put("javafx.scene.Node#focusTraversable", false);
74     ourFromSource.put("javafx.scene.Node#nodeOrientation", NodeOrientation.INHERIT);
75     ourFromSource.put("javafx.scene.Node#pickOnBounds", false);
76     ourFromSource.put("javafx.scene.SubScene#height", 0.0);
77     ourFromSource.put("javafx.scene.SubScene#width", 0.0);
78     ourFromSource.put("javafx.scene.chart.AreaChart#createSymbols", true);
79     ourFromSource.put("javafx.scene.chart.Axis#label", "");
80     ourFromSource.put("javafx.scene.chart.BarChart#barGap", 4.0);
81     ourFromSource.put("javafx.scene.chart.BarChart#categoryGap", 10.0);
82     ourFromSource.put("javafx.scene.chart.Chart#title", "");
83     ourFromSource.put("javafx.scene.chart.LineChart#axisSortingPolicy", LineChart.SortingPolicy.X_AXIS);
84     ourFromSource.put("javafx.scene.chart.LineChart#createSymbols", true);
85     ourFromSource.put("javafx.scene.chart.StackedAreaChart#createSymbols", true);
86     ourFromSource.put("javafx.scene.chart.StackedBarChart#categoryGap", 10.0);
87     ourFromSource.put("javafx.scene.chart.XYChart#alternativeColumnFillVisible", false);
88     ourFromSource.put("javafx.scene.chart.XYChart#alternativeRowFillVisible", true);
89     ourFromSource.put("javafx.scene.chart.XYChart#horizontalGridLinesVisible", true);
90     ourFromSource.put("javafx.scene.chart.XYChart#horizontalZeroLineVisible", true);
91     ourFromSource.put("javafx.scene.chart.XYChart#verticalGridLinesVisible", true);
92     ourFromSource.put("javafx.scene.chart.XYChart#verticalZeroLineVisible", true);
93     ourFromSource.put("javafx.scene.control.ComboBoxBase#editable", false);
94     ourFromSource.put("javafx.scene.control.CustomMenuItem#hideOnClick", true);
95     ourFromSource.put("javafx.scene.control.Labeled#alignment", Pos.CENTER_LEFT);
96     ourFromSource.put("javafx.scene.control.Labeled#mnemonicParsing", false);
97     ourFromSource.put("javafx.scene.control.SpinnerValueFactory#wrapAround", false);
98     ourFromSource.put("javafx.scene.control.TableSelectionModel#cellSelectionEnabled", false);
99     ourFromSource.put("javafx.scene.media.AudioClip#balance", 0.0);
100     ourFromSource.put("javafx.scene.media.AudioClip#cycleCount", 1);
101     ourFromSource.put("javafx.scene.media.AudioClip#pan", 0.0);
102     ourFromSource.put("javafx.scene.media.AudioClip#priority", 0);
103     ourFromSource.put("javafx.scene.media.AudioClip#rate", 1.0);
104     ourFromSource.put("javafx.scene.media.AudioClip#volume", 1.0);
105     ourFromSource.put("javafx.scene.media.AudioEqualizer#enabled", false);
106     ourFromSource.put("javafx.scene.media.MediaPlayer#audioSpectrumInterval", 0.1);
107     ourFromSource.put("javafx.scene.media.MediaPlayer#audioSpectrumNumBands", 128);
108     ourFromSource.put("javafx.scene.media.MediaPlayer#audioSpectrumThreshold", -60);
109     ourFromSource.put("javafx.scene.media.MediaPlayer#autoPlay", false);
110     ourFromSource.put("javafx.scene.media.MediaPlayer#balance", 0.0);
111     ourFromSource.put("javafx.scene.media.MediaPlayer#cycleCount", 1);
112     ourFromSource.put("javafx.scene.media.MediaPlayer#mute", false);
113     ourFromSource.put("javafx.scene.media.MediaPlayer#rate", 1.0);
114     ourFromSource.put("javafx.scene.media.MediaPlayer#volume", 1.0);
115     ourFromSource.put("javafx.stage.PopupWindow#anchorLocation", PopupWindow.AnchorLocation.WINDOW_TOP_LEFT);
116     ourFromSource.put("javafx.stage.PopupWindow#autoHide", false);
117     ourFromSource.put("javafx.stage.PopupWindow#consumeAutoHidingEvents", true);
118   }
119
120   private static final Set<String> ourSkippedProperties = new HashSet<>(
121     Arrays.asList("javafx.scene.web.HTMLEditor#htmlText",
122                   "javafx.scene.web.WebEngine#userAgent",
123                   "javafx.scene.control.ButtonBar#buttonOrder"));
124
125
126   /**
127    * Attempt to instantiate JavaFX classes on the FX thread, obtain default property values from instantiated objects.
128    */
129   private static void generate() {
130     System.out.println("--- JavaFX default property values ---");
131
132     final Map<String, Map<String, DefaultValue>> defaultPropertyValues = new TreeMap<>();
133     final Map<String, Map<String, String>> declaredProperties = new TreeMap<>();
134     final Map<String, Map<String, Set<String>>> overriddenProperties = new TreeMap<>();
135     final Map<String, String> superClasses = new TreeMap<>();
136     try (final ZipInputStream zip = new ZipInputStream(new FileInputStream(new File(BINARIES_PATH)))) {
137       for (ZipEntry ze = zip.getNextEntry(); ze != null; ze = zip.getNextEntry()) {
138         final String entryName = ze.getName();
139         if (!ze.isDirectory() && entryName.endsWith(".class") && entryName.startsWith("javafx")) {
140           final String className = entryName.substring(0, entryName.lastIndexOf('.')).replace('/', '.');
141           Class<?> currentClass = Class.forName(className);
142           if (!Modifier.isPublic(currentClass.getModifiers()) && !Modifier.isProtected(currentClass.getModifiers())) continue;
143           if (currentClass.getName().startsWith("javafx.beans")) continue;
144           if (currentClass.getName().matches(".*\\$\\d+$")) continue;
145           BeanInfo info = Introspector.getBeanInfo(currentClass);
146           if (currentClass.getSuperclass() != null) {
147             superClasses.put(currentClass.getName(), currentClass.getSuperclass().getName());
148           }
149
150           Set<String> bindings = new TreeSet<>();
151           for (MethodDescriptor methodDesc : info.getMethodDescriptors()) {
152             if (methodDesc.getName().endsWith("Property") &&
153                 !methodDesc.getMethod().getGenericReturnType().toString().contains("ReadOnly")) {
154               bindings.add(methodDesc.getName());
155             }
156           }
157
158           Object instance = null;
159           for (PropertyDescriptor desc : info.getPropertyDescriptors()) {
160             final String propName = desc.getName();
161             final String propQualifiedName = currentClass.getName() + "#" + propName;
162             if (ourSkippedProperties.contains(propQualifiedName)) continue;
163             final Method setter = desc.getWriteMethod();
164             final boolean hasBinding = bindings.contains(propName + "Property");
165             if (setter == null || !hasBinding) continue;
166             final Type type = setter.getGenericParameterTypes()[0];
167
168             if (type instanceof Class && isSupportedPropertyType((Class)type)) {
169               if (instance == null) {
170                 instance = instantiate(currentClass);
171                 if (instance == null) break;
172               }
173               final Object value;
174               final Method getter = desc.getReadMethod();
175               try {
176                 value = getter.invoke(instance);
177               }
178               catch (IllegalAccessException | InvocationTargetException e) {
179                 throw new RuntimeException("Can't invoke " + getter + " on " + currentClass, e);
180               }
181               if (value != null) {
182                 final Class<?> declaringClass = getter.getDeclaringClass();
183                 final DefaultValue newValue = new DefaultValue(value, declaringClass.getName());
184                 if (declaringClass.getName().startsWith("javafx")) {
185                   defaultPropertyValues
186                     .computeIfAbsent(currentClass.getName(), unused -> new TreeMap<>())
187                     .put(propName, newValue);
188                 }
189
190                 final Map<String, String> shareableProperties =
191                   declaredProperties.computeIfAbsent(declaringClass.getName(), unused -> new TreeMap<>());
192                 final String sharedValue = shareableProperties.get(propName);
193
194                 if (sharedValue == null) {
195                   shareableProperties.put(propName, newValue.getValueText());
196                 }
197                 else if (!sharedValue.equals(newValue.getValueText())) {
198                   final Set<String> multipleValues = overriddenProperties
199                     .computeIfAbsent(declaringClass.getName(), unused -> new TreeMap<>())
200                     .computeIfAbsent(propName, unused -> new TreeSet<>());
201                   multipleValues.add(sharedValue);
202                   multipleValues.add(newValue.getValueText());
203                 }
204               }
205             }
206           }
207         }
208       }
209     }
210     catch (IOException | ClassNotFoundException | IntrospectionException e) {
211       e.printStackTrace();
212     }
213
214     System.out.println("-------- Collected from sources ---------");
215     ourFromSource.forEach((qualifiedPropName, value) -> System.out.println(qualifiedPropName + "=" + value));
216
217     System.out.println("-------- Shared (not overridden) ---------");
218     declaredProperties.forEach(
219       (className, propertyMap) -> {
220         final Map<String, Set<String>> multipleValues = overriddenProperties.getOrDefault(className, Collections.emptyMap());
221         propertyMap.forEach((propName, valueText) -> {
222           if (!multipleValues.containsKey(propName)) {
223             System.out.println(className + "#" + propName + "=" + valueText);
224           }
225         });
226       });
227     System.out.println("-------- Overridden in subclass ---------");
228     final Map<String, Map<String, DefaultValue>> fromSource = new TreeMap<>();
229     ourFromSource.forEach((qualifiedPropName, value) -> {
230       final int p = qualifiedPropName.indexOf('#');
231       if (p > 0 && p < qualifiedPropName.length()) {
232         final String className = qualifiedPropName.substring(0, p);
233         final String propName = qualifiedPropName.substring(p + 1);
234         fromSource.computeIfAbsent(className, unused -> new TreeMap<>())
235           .computeIfAbsent(propName, unused -> new DefaultValue(value, className));
236       }
237     });
238     defaultPropertyValues.forEach(
239       (className, propertyMap) -> propertyMap.forEach(
240         (propName, propValue) -> {
241           final DefaultValue sourceValue = fromSource.getOrDefault(className, Collections.emptyMap()).get(propName);
242           if (sourceValue != null && areValuesEqual(propValue, sourceValue)) {
243             return;
244           }
245           final Map<String, Set<String>> multipleValues = overriddenProperties.get(propValue.getDeclaringClass());
246           if (multipleValues != null && multipleValues.get(propName) != null) {
247             boolean sameValueInSuperClass = false;
248             for (String scName = superClasses.get(className); scName != null; scName = superClasses.get(scName)) {
249               final Map<String, DefaultValue> superPropMap = defaultPropertyValues.getOrDefault(scName, fromSource.get(scName));
250               if (superPropMap != null) {
251                 DefaultValue superValue = superPropMap.get(propName);
252                 if (superValue != null) {
253                   sameValueInSuperClass = areValuesEqual(propValue, superValue);
254                   break;
255                 }
256               }
257             }
258             if (!sameValueInSuperClass) {
259               System.out.println(className + "#" + propName + "=" + propValue.getValueText());
260             }
261           }
262         }));
263
264     System.out.println("-------- Overridden properties' values ---------");
265     overriddenProperties
266       .forEach((className, propertyValues) -> propertyValues.entrySet()
267         .forEach(e -> System.out.println("-- " + className + "#" + e.getKey() + e.getValue())));
268     System.out.println("-------- Skipped properties ---------");
269     ourSkippedProperties.forEach(propName -> System.out.println("-- " + propName));
270   }
271
272   private static boolean areValuesEqual(DefaultValue first, DefaultValue second) {
273     return second.getValueText().equals(first.getValueText());
274   }
275
276   private static boolean isSupportedPropertyType(Class<?> propertyClass) {
277     return propertyClass.isPrimitive() ||
278            propertyClass.isEnum() ||
279            Number.class.isAssignableFrom(propertyClass) ||
280            Boolean.class.isAssignableFrom(propertyClass) ||
281            Character.class.isAssignableFrom(propertyClass) ||
282            CharSequence.class.isAssignableFrom(propertyClass);
283   }
284
285   private static Object instantiate(Class<?> aClass) {
286     try {
287       if (Modifier.isAbstract(aClass.getModifiers())) return null;
288
289       final Constructor<?> constructor;
290       try {
291         constructor = aClass.getConstructor();
292       }
293       catch (NoSuchMethodException e) {
294         return null;
295       }
296       constructor.setAccessible(true);
297       return constructor.newInstance();
298     }
299     catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
300       System.err.println("Not instantiated " + aClass + " " + e);
301       return null;
302     }
303   }
304
305   private static final Set<String> ourSourceClasses = new TreeSet<>();
306
307   static {
308     ourSourceClasses.add("javafx.animation.Animation");
309     ourSourceClasses.add("javafx.concurrent.ScheduledService");
310     ourSourceClasses.add("javafx.print.JobSettings");
311     ourSourceClasses.add("javafx.scene.Camera");
312     ourSourceClasses.add("javafx.scene.LightBase");
313     ourSourceClasses.add("javafx.scene.Node");
314     ourSourceClasses.add("javafx.scene.Scene");
315     ourSourceClasses.add("javafx.scene.SubScene");
316     ourSourceClasses.add("javafx.scene.canvas.GraphicsContext");
317     ourSourceClasses.add("javafx.scene.chart.AreaChart");
318     ourSourceClasses.add("javafx.scene.chart.Axis");
319     ourSourceClasses.add("javafx.scene.chart.BarChart");
320     ourSourceClasses.add("javafx.scene.chart.Chart");
321     ourSourceClasses.add("javafx.scene.chart.LineChart");
322     ourSourceClasses.add("javafx.scene.chart.PieChart");
323     ourSourceClasses.add("javafx.scene.chart.StackedAreaChart");
324     ourSourceClasses.add("javafx.scene.chart.StackedBarChart");
325     ourSourceClasses.add("javafx.scene.chart.ValueAxis");
326     ourSourceClasses.add("javafx.scene.chart.XYChart");
327     ourSourceClasses.add("javafx.scene.control.Alert");
328     ourSourceClasses.add("javafx.scene.control.ComboBoxBase");
329     ourSourceClasses.add("javafx.scene.control.Labeled");
330     ourSourceClasses.add("javafx.scene.control.MultipleSelectionModel");
331     ourSourceClasses.add("javafx.scene.control.SpinnerValueFactory");
332     ourSourceClasses.add("javafx.scene.control.SpinnerValueFactory");
333     ourSourceClasses.add("javafx.scene.control.SpinnerValueFactory");
334     ourSourceClasses.add("javafx.scene.control.TableColumnBase");
335     ourSourceClasses.add("javafx.scene.control.TableSelectionModel");
336     ourSourceClasses.add("javafx.scene.control.TextFormatter");
337     ourSourceClasses.add("javafx.scene.control.TextInputControl");
338     ourSourceClasses.add("javafx.scene.control.Toggle");
339     ourSourceClasses.add("javafx.scene.input.DragEvent");
340     ourSourceClasses.add("javafx.scene.input.Dragboard");
341     ourSourceClasses.add("javafx.scene.input.MouseEvent");
342     ourSourceClasses.add("javafx.scene.media.AudioClip");
343     ourSourceClasses.add("javafx.scene.media.AudioEqualizer");
344     ourSourceClasses.add("javafx.scene.media.MediaPlayer");
345     ourSourceClasses.add("javafx.scene.shape.PathElement");
346     ourSourceClasses.add("javafx.scene.shape.Shape");
347     ourSourceClasses.add("javafx.scene.shape.Shape3D");
348     ourSourceClasses.add("javafx.scene.web.WebHistory");
349     ourSourceClasses.add("javafx.stage.PopupWindow");
350     ourSourceClasses.add("javafx.stage.Window");
351   }
352
353   private static void scanSource() {
354
355     Pattern defaultValueJavaDoc = Pattern.compile("^.*\\*\\s*@defaultValue\\s*(.+)\\s*$");
356     Pattern fieldDecl = Pattern.compile("^.*\\s(\\w+)\\s*(=.*|;)\\s*$");
357     Pattern methodDecl = Pattern.compile("^.*\\s(\\w+)\\s*\\(\\).*$");
358     Pattern propertyDecl = Pattern.compile("^.*Property\\S*\\s+(\\w+)\\s*=\\s*(.+)[;{].*$");
359
360     Map<String, String> props = new TreeMap<>();
361     try (final ZipInputStream zip = new ZipInputStream(new FileInputStream(new File(SOURCE_PATH)))) {
362       byte[] buffer = new byte[1 << 16];
363       for (ZipEntry ze = zip.getNextEntry(); ze != null; ze = zip.getNextEntry()) {
364         final String eName = ze.getName();
365         if (!ze.isDirectory() && eName.endsWith(".java") && eName.startsWith("javafx")) {
366           String className = eName.substring(0, eName.lastIndexOf('.')).replace('/', '.');
367           if (ourSourceClasses.contains(className)) {
368             StringBuilder text = new StringBuilder();
369             int len;
370             while ((len = zip.read(buffer)) > 0) {
371               String str = new String(buffer, 0, len);
372               text.append(str);
373             }
374             String[] lines = text.toString().split("\n");
375             int state = 0;
376             String name = null;
377             String value = null;
378             for (String s : lines) {
379               Matcher m;
380               m = propertyDecl.matcher(s);
381               if (m.matches()) {
382                 name = m.group(1);
383                 value = m.group(2);
384                 if (!"null".equals(value)) {
385                   props.put(className + "#" + name, value);
386                 }
387               }
388               switch (state) {
389                 case 0:
390                   m = defaultValueJavaDoc.matcher(s);
391                   if (m.matches()) {
392                     state = 1;
393                     value = m.group(1);
394                   }
395                   break;
396                 case 1:
397                   if (s.contains("*/")) state = 2;
398                   break;
399                 case 2:
400                   if (!s.trim().isEmpty()) {
401                     state = 0;
402                     m = fieldDecl.matcher(s);
403                     if (m.matches()) {
404                       name = m.group(1);
405                     }
406                     else {
407                       m = methodDecl.matcher(s);
408                       if (m.matches()) {
409                         name = m.group(1);
410                       }
411                     }
412                     if (name != null && value != null && !"null".equals(value)) {
413                       props.put(className + "#" + name, value);
414                     }
415                     name = value = null;
416                   }
417                   break;
418               }
419             }
420           }
421         }
422       }
423     }
424     catch (IOException e) {
425       System.err.println("Failed to read sources " + e);
426     }
427     System.out.println("--- Default values collected from JavaDoc and declarations. To be reviewed and manually edited ---");
428     props.forEach((n, v) -> System.out.println(n + "=" + v));
429   }
430
431   private static class DefaultValue {
432     private final String myValueText;
433     private final String myDeclaringClass;
434
435     public DefaultValue(@NotNull Object value, @NotNull String declaringClass) {
436       myValueText = String.valueOf(value);
437       myDeclaringClass = declaringClass;
438     }
439
440     public String getDeclaringClass() {
441       return myDeclaringClass;
442     }
443
444     public String getValueText() {
445       return myValueText;
446     }
447
448     @Override
449     public String toString() {
450       return myValueText;
451     }
452   }
453 }