5bfeb25f907821317ed9595c8b8a0965c8127b3b
[javainspect.git] / src / main / java / eu / svjatoslav / inspector / java / structure / ClassDescriptor.java
1 /*
2  * JavaInspect - Utility to visualize java software
3  * Copyright (C) 2013-2017, Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of version 3 of the GNU Lesser General Public License
7  * or later as published by the Free Software Foundation.
8  */
9
10 package eu.svjatoslav.inspector.java.structure;
11
12 import java.lang.reflect.Field;
13 import java.lang.reflect.Method;
14 import java.util.*;
15
16 /**
17  * Describes single class instance
18  */
19 public class ClassDescriptor implements GraphElement, Comparable<ClassDescriptor> {
20
21     private static final int MAX_REFERECNES_COUNT = 10;
22     private final Map<String, FieldDescriptor> nameToFieldMap = new TreeMap<>();
23     private final SortedSet<MethodDescriptor> methods = new TreeSet<MethodDescriptor>();
24     private final ClassGraph classGraph;
25     boolean isEnum;
26     boolean isInterface;
27     boolean isArray;
28     List<ClassDescriptor> interfaces = new ArrayList<ClassDescriptor>();
29     ClassDescriptor superClass;
30     private String fullyQualifiedName;
31     /**
32      * Incoming arrows will have this color.
33      */
34     private String distinctiveReferenceColor;
35     private String interfaceColor;
36     private String superClassColor;
37     private boolean isShown = true;
38     /**
39      * Amount of field and method references pointing to this class.
40      */
41     private int referencesCount = 0;
42
43     // for interface, counts amount of found implementations
44     private int implementationsCount = 0;
45
46     // counts amount of times this class is extended
47     private int extensionsCount = 0;
48
49     private ClassDescriptor arrayComponent;
50
51     public ClassDescriptor(final ClassGraph classGraph) {
52         this.classGraph = classGraph;
53     }
54
55     protected void analyzeClass(final Class<? extends Object> clazz) {
56
57         fullyQualifiedName = clazz.getName();
58
59         isArray = clazz.isArray();
60
61         if (isArray) {
62             final Class<?> componentType = clazz.getComponentType();
63             arrayComponent = getClassGraph().getOrCreateClassDescriptor(
64                     componentType);
65         }
66
67         // System.out.println("class: " + fullyQualifiedName);
68
69         isEnum = clazz.isEnum();
70
71         isInterface = clazz.isInterface();
72
73         if (!isVisible())
74             return;
75
76         try {
77             indexFields(clazz.getDeclaredFields());
78             indexFields(clazz.getFields());
79         } catch (NoClassDefFoundError error){
80             // TODO: better logging of this error
81             System.out.println(error.toString());
82         }
83
84         indexMethods(clazz);
85
86         for (final Class interfaceClass : clazz.getInterfaces()) {
87             final ClassDescriptor interfaceClassDescriptor = getClassGraph()
88                     .getOrCreateClassDescriptor(interfaceClass);
89             interfaceClassDescriptor.registerImplementation();
90             interfaces.add(interfaceClassDescriptor);
91         }
92
93         superClass = getClassGraph().getOrCreateClassDescriptor(
94                 clazz.getSuperclass());
95         if (superClass != null)
96             superClass.registerExtension();
97
98     }
99
100     protected boolean areReferencesShown() {
101         return referencesCount <= MAX_REFERECNES_COUNT;
102     }
103
104     private void enlistFieldReferences(final StringBuffer result) {
105         if (nameToFieldMap.isEmpty())
106             return;
107
108         result.append("\n");
109         result.append("    // field references to other classes\n");
110         nameToFieldMap.forEach((fieldName, field) -> result.append(field.getDot()));
111     }
112
113     private void enlistFields(final StringBuffer result) {
114         if (nameToFieldMap.isEmpty())
115             return;
116
117         result.append("\n");
118         result.append("    // fields:\n");
119
120         // enlist fields
121         for (final Map.Entry<String, FieldDescriptor> entry : nameToFieldMap
122                 .entrySet())
123             result.append(entry.getValue().getEmbeddedDot());
124     }
125
126     private void enlistImplementedInterfaces(final StringBuffer result) {
127         if (interfaces.isEmpty())
128             return;
129
130         result.append("\n");
131         result.append("    // interfaces implemented by class: "
132                 + fullyQualifiedName + "\n");
133
134         for (final ClassDescriptor interfaceDescriptor : interfaces) {
135             if (!interfaceDescriptor.isVisible())
136                 continue;
137
138             if (!interfaceDescriptor.areReferencesShown())
139                 continue;
140
141             result.append("    " + interfaceDescriptor.getGraphId() + " -> "
142                     + getGraphId() + "[style=\"dotted\", color=\""
143                     + interfaceDescriptor.getInterfaceColor()
144                     + "\", penwidth=10, dir=\"forward\"];\n");
145         }
146     }
147
148     private void enlistMethodReferences(final StringBuffer result) {
149         if (methods.isEmpty())
150             return;
151
152         result.append("\n");
153         result.append("    // method references to other classes\n");
154         for (final MethodDescriptor methodDescriptor : methods)
155             result.append(methodDescriptor.getDot());
156     }
157
158     private void enlistMethods(final StringBuffer result) {
159         if (methods.isEmpty())
160             return;
161
162         result.append("\n");
163         result.append("    // methods:\n");
164
165         // enlist methods
166         for (final MethodDescriptor methodDescriptor : methods)
167             result.append(methodDescriptor.getEmbeddedDot());
168     }
169
170     private void enlistSuperClass(final StringBuffer result) {
171         if (superClass == null)
172             return;
173
174         if (!superClass.isVisible())
175             return;
176
177         if (!superClass.areReferencesShown())
178             return;
179
180         result.append("\n");
181         result.append("    // super class for: " + fullyQualifiedName + "\n");
182
183         result.append("    " + superClass.getGraphId() + " -> " + getGraphId()
184                 + "[ color=\"" + superClass.getSuperClassColor()
185                 + "\", penwidth=10, dir=\"forward\"];\n");
186     }
187
188     private void generateDotHeader(final StringBuffer result) {
189         result.append("\n");
190         result.append("// Class: " + fullyQualifiedName + "\n");
191
192         result.append("    " + getGraphId() + "[label=<<TABLE "
193                 + getBackgroundColor() + " BORDER=\"" + getBorderWidth()
194                 + "\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n");
195
196         result.append("\n");
197         result.append("    // class descriptor header\n");
198         result.append("    <TR><TD colspan=\"2\" PORT=\"f0\">"
199                 + "<FONT POINT-SIZE=\"8.0\" >" + getPackageName()
200                 + "</FONT><br/>");
201
202         final String parentClassesName = getParentClassesName();
203         if (parentClassesName.length() > 0)
204             result.append("<FONT POINT-SIZE=\"12.0\"><B>" + parentClassesName
205                     + "</B></FONT><br/>\n");
206
207         result.append("<FONT POINT-SIZE=\"25.0\"><B>" + getClassName(false)
208                 + "</B></FONT>" + "</TD></TR>\n");
209     }
210
211     private String getBackgroundColor() {
212         String bgColor = "";
213
214         if (isEnum)
215             bgColor = "bgcolor=\"navajowhite2\"";
216
217         if (isInterface)
218             bgColor = "bgcolor=\"darkslategray1\"";
219
220         return bgColor;
221     }
222
223     private String getBorderWidth() {
224
225         if (!areReferencesShown())
226             return "4";
227         return "1";
228     }
229
230     protected ClassGraph getClassGraph() {
231         return classGraph;
232     }
233
234     protected String getClassName(final boolean differentiateArray) {
235         // this is needed for nested classes
236         final String actualClassName = fullyQualifiedName.replace('$', '.');
237
238         String result;
239         if (isArray) {
240             // for arrays use array component instead of array class name
241             result = arrayComponent.fullyQualifiedName;
242             if (result.contains(".")) {
243                 final int i = result.lastIndexOf('.');
244                 result = result.substring(i + 1);
245             }
246         } else {
247             final int i = actualClassName.lastIndexOf('.');
248             result = actualClassName.substring(i + 1);
249         }
250
251         if (differentiateArray)
252             if (isArray)
253                 result += " []";
254
255         // this is needed for nested classes
256         // result = result.replace('$', '.');
257         return result;
258     }
259
260     protected String getColor() {
261         if (distinctiveReferenceColor == null)
262             distinctiveReferenceColor = Utils.getNextDarkColor();
263
264         return distinctiveReferenceColor;
265     }
266
267     @Override
268     public String getDot() {
269         if (!isVisible())
270             return "";
271
272         if (isArray)
273             return "";
274
275         final StringBuffer result = new StringBuffer();
276
277         generateDotHeader(result);
278
279         enlistFields(result);
280
281         enlistMethods(result);
282
283         result.append("    </TABLE>>, shape=\"none\"];\n");
284
285         enlistFieldReferences(result);
286
287         enlistMethodReferences(result);
288
289         enlistImplementedInterfaces(result);
290
291         enlistSuperClass(result);
292
293         return result.toString();
294     }
295
296     @Override
297     public String getEmbeddedDot() {
298         return null;
299     }
300
301     /**
302      * Returns field with given name (case is ignored). Or <code>null</code> if
303      * field is not found.
304      *
305      * @param fieldToSearch field name (case is ignored)
306      * @return field matching given name
307      */
308     protected FieldDescriptor getFieldIgnoreCase(final String fieldToSearch) {
309
310         for (final String fieldName : nameToFieldMap.keySet())
311             if (fieldToSearch.equalsIgnoreCase(fieldName))
312                 return nameToFieldMap.get(fieldName);
313
314         return null;
315     }
316
317     protected String getFullyQualifiedName() {
318         return fullyQualifiedName;
319     }
320
321     @Override
322     public String getGraphId() {
323         final String result = "class_"
324                 + fullyQualifiedName
325                 .replace('.', '_')
326                 .replace(";", "")
327                 .replace("[[", "")
328                 .replace("[L", "")
329                 .replace("[[L", "") // array of arrays
330                 .replace("[[[L", "") // array of arrays of arrays
331                 .replace('$', '_');
332
333         return result;
334     }
335
336     private String getInterfaceColor() {
337         if (interfaceColor == null)
338             interfaceColor = Utils.getNextDarkColor();
339
340         return interfaceColor;
341     }
342
343     private FieldDescriptor getOrCreateFieldDescriptor(final Field field) {
344
345         final String fieldName = field.getName();
346
347         if (nameToFieldMap.containsKey(fieldName))
348             return nameToFieldMap.get(fieldName);
349
350         final FieldDescriptor newFieldDescriptor = new FieldDescriptor(this);
351         nameToFieldMap.put(fieldName, newFieldDescriptor);
352
353         newFieldDescriptor.analyzeField(field);
354
355         return newFieldDescriptor;
356     }
357
358     private int getOutgoingReferencesCount() {
359         int result = 0;
360
361         // count method references
362         for (final MethodDescriptor methodDescriptor : methods)
363             result += methodDescriptor.getOutsideVisibleReferencesCount();
364
365         // count field references
366         for (final FieldDescriptor fieldDescriptor : nameToFieldMap.values())
367             result += fieldDescriptor.getOutsideVisibleReferencesCount();
368
369         // count implemented interfaces
370         for (final ClassDescriptor classDescriptor : interfaces)
371             if (classDescriptor.isVisible())
372                 result++;
373
374         // count superclass
375         if (superClass != null)
376             if (superClass.isVisible())
377                 result++;
378
379         return result;
380     }
381
382     private String getPackageName() {
383
384         final int i = fullyQualifiedName.lastIndexOf('.');
385
386         if (i == -1)
387             return "";
388
389         return fullyQualifiedName.substring(0, i).replace("[L", "");
390     }
391
392     private String getParentClassesName() {
393         int i = fullyQualifiedName.lastIndexOf('.');
394         final String fullClassName = fullyQualifiedName.substring(i + 1);
395
396         i = fullClassName.lastIndexOf('$');
397         if (i == -1)
398             return "";
399         final String parentClassesName = fullClassName.substring(0, i);
400         return parentClassesName.replace('$', '.');
401     }
402
403     private String getSuperClassColor() {
404         if (superClassColor == null)
405             superClassColor = Utils.getNextLightColor();
406
407         return superClassColor;
408     }
409
410     /**
411      * Checks if class has field with given name (case is ignored). Returns
412      * <code>true</code> if such field is found.
413      *
414      * @param fieldToSearch field to search for (case is ignored)
415      * @return <code>true</code> if field is found.
416      */
417     protected boolean hasFieldIgnoreCase(final String fieldToSearch) {
418
419         for (final String fieldName : nameToFieldMap.keySet())
420             if (fieldToSearch.equalsIgnoreCase(fieldName))
421                 return true;
422
423         return false;
424     }
425
426     private void hide() {
427         isShown = false;
428     }
429
430     protected void hideClassIfNoReferences() {
431         if (!isVisible())
432             return;
433
434         final int totalReferencesCount = getOutgoingReferencesCount()
435                 + referencesCount + extensionsCount + implementationsCount;
436
437         if (totalReferencesCount == 0) {
438             hide();
439             return;
440         }
441
442         return;
443     }
444
445     private void indexFields(final Field[] fields) {
446         for (final Field field : fields)
447             getOrCreateFieldDescriptor(field);
448     }
449
450     private void indexMethods(final Class<? extends Object> clazz) {
451         for (final Method method : clazz.getMethods()) {
452             final MethodDescriptor methodDescriptor = new MethodDescriptor(
453                     this, method.getName());
454
455             methods.add(methodDescriptor);
456
457             methodDescriptor.analyze(method);
458         }
459
460     }
461
462     @Override
463     public boolean isVisible() {
464
465         if (Utils.isSystemDataType(fullyQualifiedName))
466             return false;
467
468         if (Utils.isSystemPackage(fullyQualifiedName))
469             return false;
470
471         if (!getClassGraph().isClassShown(fullyQualifiedName))
472             return false;
473
474         if (isArray)
475             if (arrayComponent != null)
476                 if (Utils.isSystemDataType(arrayComponent.fullyQualifiedName))
477                     // Do not show references to primitive data types in arrays.
478                     // That is: there is no point to show reference to byte when
479                     // we have class with byte array field.
480                     return false;
481
482         return isShown;
483     }
484
485     /**
486      * Register event when another class is extending this one.
487      */
488     protected void registerExtension() {
489         extensionsCount++;
490     }
491
492     protected void registerImplementation() {
493         implementationsCount++;
494     }
495
496     protected void registerReference() {
497         referencesCount++;
498     }
499
500     @Override
501     public boolean equals(Object o) {
502         if (this == o) return true;
503         if (!(o instanceof ClassDescriptor)) return false;
504
505         ClassDescriptor that = (ClassDescriptor) o;
506
507         return getFullyQualifiedName().equals(that.getFullyQualifiedName());
508
509     }
510
511     @Override
512     public int hashCode() {
513         return getFullyQualifiedName().hashCode();
514     }
515
516     @Override
517     public int compareTo(ClassDescriptor o) {
518         return fullyQualifiedName.compareTo(o.fullyQualifiedName);
519     }
520 }