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