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