6a941304da0782aef9961208bb86c4ff5ddcd861
[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<String, FieldDescriptor>();
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         for (final Map.Entry<String, FieldDescriptor> entry : nameToFieldMap
111                 .entrySet())
112             result.append(entry.getValue().getDot());
113     }
114
115     private void enlistFields(final StringBuffer result) {
116         if (nameToFieldMap.isEmpty())
117             return;
118
119         result.append("\n");
120         result.append("    // fields:\n");
121
122         // enlist fields
123         for (final Map.Entry<String, FieldDescriptor> entry : nameToFieldMap
124                 .entrySet())
125             result.append(entry.getValue().getEmbeddedDot());
126     }
127
128     private void enlistImplementedInterfaces(final StringBuffer result) {
129         if (interfaces.isEmpty())
130             return;
131
132         result.append("\n");
133         result.append("    // interfaces implemented by class: "
134                 + fullyQualifiedName + "\n");
135
136         for (final ClassDescriptor interfaceDescriptor : interfaces) {
137             if (!interfaceDescriptor.isVisible())
138                 continue;
139
140             if (!interfaceDescriptor.areReferencesShown())
141                 continue;
142
143             result.append("    " + interfaceDescriptor.getGraphId() + " -> "
144                     + getGraphId() + "[style=\"dotted\", color=\""
145                     + interfaceDescriptor.getInterfaceColor()
146                     + "\", penwidth=10, dir=\"forward\"];\n");
147         }
148     }
149
150     private void enlistMethodReferences(final StringBuffer result) {
151         if (methods.isEmpty())
152             return;
153
154         result.append("\n");
155         result.append("    // method references to other classes\n");
156         for (final MethodDescriptor methodDescriptor : methods)
157             result.append(methodDescriptor.getDot());
158     }
159
160     private void enlistMethods(final StringBuffer result) {
161         if (methods.isEmpty())
162             return;
163
164         result.append("\n");
165         result.append("    // methods:\n");
166
167         // enlist methods
168         for (final MethodDescriptor methodDescriptor : methods)
169             result.append(methodDescriptor.getEmbeddedDot());
170     }
171
172     private void enlistSuperClass(final StringBuffer result) {
173         if (superClass == null)
174             return;
175
176         if (!superClass.isVisible())
177             return;
178
179         if (!superClass.areReferencesShown())
180             return;
181
182         result.append("\n");
183         result.append("    // super class for: " + fullyQualifiedName + "\n");
184
185         result.append("    " + superClass.getGraphId() + " -> " + getGraphId()
186                 + "[ color=\"" + superClass.getSuperClassColor()
187                 + "\", penwidth=10, dir=\"forward\"];\n");
188     }
189
190     private void generateDotHeader(final StringBuffer result) {
191         result.append("\n");
192         result.append("// Class: " + fullyQualifiedName + "\n");
193
194         result.append("    " + getGraphId() + "[label=<<TABLE "
195                 + getBackgroundColor() + " BORDER=\"" + getBorderWidth()
196                 + "\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n");
197
198         result.append("\n");
199         result.append("    // class descriptor header\n");
200         result.append("    <TR><TD colspan=\"2\" PORT=\"f0\">"
201                 + "<FONT POINT-SIZE=\"8.0\" >" + getPackageName()
202                 + "</FONT><br/>");
203
204         final String parentClassesName = getParentClassesName();
205         if (parentClassesName.length() > 0)
206             result.append("<FONT POINT-SIZE=\"12.0\"><B>" + parentClassesName
207                     + "</B></FONT><br/>\n");
208
209         result.append("<FONT POINT-SIZE=\"25.0\"><B>" + getClassName(false)
210                 + "</B></FONT>" + "</TD></TR>\n");
211     }
212
213     private String getBackgroundColor() {
214         String bgColor = "";
215
216         if (isEnum)
217             bgColor = "bgcolor=\"navajowhite2\"";
218
219         if (isInterface)
220             bgColor = "bgcolor=\"darkslategray1\"";
221
222         return bgColor;
223     }
224
225     private String getBorderWidth() {
226
227         if (!areReferencesShown())
228             return "4";
229         return "1";
230     }
231
232     protected ClassGraph getClassGraph() {
233         return classGraph;
234     }
235
236     protected String getClassName(final boolean differentiateArray) {
237         // this is needed for nested classes
238         final String actualClassName = fullyQualifiedName.replace('$', '.');
239
240         String result;
241         if (isArray) {
242             // for arrays use array component instead of array class name
243             result = arrayComponent.fullyQualifiedName;
244             if (result.contains(".")) {
245                 final int i = result.lastIndexOf('.');
246                 result = result.substring(i + 1);
247             }
248         } else {
249             final int i = actualClassName.lastIndexOf('.');
250             result = actualClassName.substring(i + 1);
251         }
252
253         if (differentiateArray)
254             if (isArray)
255                 result += " []";
256
257         // this is needed for nested classes
258         // result = result.replace('$', '.');
259         return result;
260     }
261
262     protected String getColor() {
263         if (distinctiveReferenceColor == null)
264             distinctiveReferenceColor = Utils.getNextDarkColor();
265
266         return distinctiveReferenceColor;
267     }
268
269     @Override
270     public String getDot() {
271         if (!isVisible())
272             return "";
273
274         if (isArray)
275             return "";
276
277         final StringBuffer result = new StringBuffer();
278
279         generateDotHeader(result);
280
281         enlistFields(result);
282
283         enlistMethods(result);
284
285         result.append("    </TABLE>>, shape=\"none\"];\n");
286
287         enlistFieldReferences(result);
288
289         enlistMethodReferences(result);
290
291         enlistImplementedInterfaces(result);
292
293         enlistSuperClass(result);
294
295         return result.toString();
296     }
297
298     @Override
299     public String getEmbeddedDot() {
300         return null;
301     }
302
303     /**
304      * Returns field with given name (case is ignored). Or <code>null</code> if
305      * field is not found.
306      *
307      * @param fieldToSearch field name (case is ignored)
308      * @return field matching given name
309      */
310     protected FieldDescriptor getFieldIgnoreCase(final String fieldToSearch) {
311
312         for (final String fieldName : nameToFieldMap.keySet())
313             if (fieldToSearch.equalsIgnoreCase(fieldName))
314                 return nameToFieldMap.get(fieldName);
315
316         return null;
317     }
318
319     protected String getFullyQualifiedName() {
320         return fullyQualifiedName;
321     }
322
323     @Override
324     public String getGraphId() {
325         final String result = "class_"
326                 + fullyQualifiedName
327                 .replace('.', '_')
328                 .replace(";", "")
329                 .replace("[L", "")
330                 .replace("[[", "")
331                 .replace('$', '_');
332         return result;
333     }
334
335     private String getInterfaceColor() {
336         if (interfaceColor == null)
337             interfaceColor = Utils.getNextDarkColor();
338
339         return interfaceColor;
340     }
341
342     private FieldDescriptor getOrCreateFieldDescriptor(final Field field) {
343
344         final String fieldName = field.getName();
345
346         if (nameToFieldMap.containsKey(fieldName))
347             return nameToFieldMap.get(fieldName);
348
349         final FieldDescriptor newFieldDescriptor = new FieldDescriptor(this);
350         nameToFieldMap.put(fieldName, newFieldDescriptor);
351
352         newFieldDescriptor.analyzeField(field);
353
354         return newFieldDescriptor;
355     }
356
357     private int getOutgoingReferencesCount() {
358         int result = 0;
359
360         // count method references
361         for (final MethodDescriptor methodDescriptor : methods)
362             result += methodDescriptor.getOutsideVisibleReferencesCount();
363
364         // count field references
365         for (final FieldDescriptor fieldDescriptor : nameToFieldMap.values())
366             result += fieldDescriptor.getOutsideVisibleReferencesCount();
367
368         // count implemented interfaces
369         for (final ClassDescriptor classDescriptor : interfaces)
370             if (classDescriptor.isVisible())
371                 result++;
372
373         // count superclass
374         if (superClass != null)
375             if (superClass.isVisible())
376                 result++;
377
378         return result;
379     }
380
381     private String getPackageName() {
382
383         final int i = fullyQualifiedName.lastIndexOf('.');
384
385         if (i == -1)
386             return "";
387
388         return fullyQualifiedName.substring(0, i).replace("[L", "");
389     }
390
391     private String getParentClassesName() {
392         int i = fullyQualifiedName.lastIndexOf('.');
393         final String fullClassName = fullyQualifiedName.substring(i + 1);
394
395         i = fullClassName.lastIndexOf('$');
396         if (i == -1)
397             return "";
398         final String parentClassesName = fullClassName.substring(0, i);
399         return parentClassesName.replace('$', '.');
400     }
401
402     private String getSuperClassColor() {
403         if (superClassColor == null)
404             superClassColor = Utils.getNextLightColor();
405
406         return superClassColor;
407     }
408
409     /**
410      * Checks if class has field with given name (case is ignored). Returns
411      * <code>true</code> if such field is found.
412      *
413      * @param fieldToSearch field to search for (case is ignored)
414      * @return <code>true</code> if field is found.
415      */
416     protected boolean hasFieldIgnoreCase(final String fieldToSearch) {
417
418         for (final String fieldName : nameToFieldMap.keySet())
419             if (fieldToSearch.equalsIgnoreCase(fieldName))
420                 return true;
421
422         return false;
423     }
424
425     private void hide() {
426         isShown = false;
427     }
428
429     protected void hideClassIfNoReferences() {
430         if (!isVisible())
431             return;
432
433         final int totalReferencesCount = getOutgoingReferencesCount()
434                 + referencesCount + extensionsCount + implementationsCount;
435
436         if (totalReferencesCount == 0) {
437             hide();
438             return;
439         }
440
441         return;
442     }
443
444     private void indexFields(final Field[] fields) {
445         for (final Field field : fields)
446             getOrCreateFieldDescriptor(field);
447     }
448
449     private void indexMethods(final Class<? extends Object> clazz) {
450         for (final Method method : clazz.getMethods()) {
451             final MethodDescriptor methodDescriptor = new MethodDescriptor(
452                     this, method.getName());
453
454             methods.add(methodDescriptor);
455
456             methodDescriptor.analyze(method);
457         }
458
459     }
460
461     @Override
462     public boolean isVisible() {
463
464         if (Utils.isSystemDataType(fullyQualifiedName))
465             return false;
466
467         if (Utils.isSystemPackage(fullyQualifiedName))
468             return false;
469
470         if (!getClassGraph().isClassShown(fullyQualifiedName))
471             return false;
472
473         if (isArray)
474             if (arrayComponent != null)
475                 if (Utils.isSystemDataType(arrayComponent.fullyQualifiedName))
476                     // Do not show references to primitive data types in arrays.
477                     // That is: there is no point to show reference to byte when
478                     // we have class with byte array field.
479                     return false;
480
481         return isShown;
482     }
483
484     /**
485      * Register event when another class is extending this one.
486      */
487     protected void registerExtension() {
488         extensionsCount++;
489     }
490
491     protected void registerImplementation() {
492         implementationsCount++;
493     }
494
495     protected void registerReference() {
496         referencesCount++;
497     }
498
499     @Override
500     public boolean equals(Object o) {
501         if (this == o) return true;
502         if (!(o instanceof ClassDescriptor)) return false;
503
504         ClassDescriptor that = (ClassDescriptor) o;
505
506         return getFullyQualifiedName().equals(that.getFullyQualifiedName());
507
508     }
509
510     @Override
511     public int hashCode() {
512         return getFullyQualifiedName().hashCode();
513     }
514
515     @Override
516     public int compareTo(ClassDescriptor o) {
517         return fullyQualifiedName.compareTo(o.fullyQualifiedName);
518     }
519 }