1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.templating.inspector;
35
36 import info.magnolia.jcr.util.ContentMap;
37 import info.magnolia.jcr.util.NodeUtil;
38 import info.magnolia.jcr.util.PropertyUtil;
39 import info.magnolia.templating.inspector.formatter.HtmlOutputter;
40 import info.magnolia.templating.inspector.formatter.Outputter;
41 import info.magnolia.templating.inspector.formatter.TextOutputter;
42 import info.magnolia.templating.inspector.spi.ValueFormatter;
43
44 import java.lang.reflect.Array;
45 import java.util.Calendar;
46 import java.util.Collection;
47 import java.util.Comparator;
48 import java.util.Date;
49 import java.util.HashMap;
50 import java.util.LinkedHashMap;
51 import java.util.Map;
52 import java.util.TreeMap;
53 import java.util.stream.Collectors;
54
55 import javax.jcr.Node;
56 import javax.jcr.Property;
57 import javax.jcr.PropertyIterator;
58 import javax.jcr.RepositoryException;
59 import javax.jcr.Value;
60
61 import org.apache.commons.lang3.ObjectUtils;
62 import org.apache.commons.lang3.StringEscapeUtils;
63 import org.apache.commons.lang3.StringUtils;
64 import org.apache.commons.lang3.time.FastDateFormat;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import lombok.SneakyThrows;
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97 public class Inspector {
98
99 private static final Logger log = LoggerFactory.getLogger(Inspector.class);
100
101 private static final Comparator<Object> KEY_COMPARATOR = (o1, o2) -> {
102
103 if (o1 instanceof String && o2 instanceof String) {
104 String str1 = (String) o1;
105 String str2 = (String) o2;
106 if ((str1.startsWith("jcr:") && str2.startsWith("jcr:")) || (str1.startsWith("mgnl:") && str2.startsWith("mgnl:"))) {
107 return str1.compareTo(str2);
108 } else if (str1.startsWith("jcr:")) {
109 return 1;
110 } else if (str2.startsWith("jcr:")) {
111 return -1;
112 } else if (str1.startsWith("mgnl:")) {
113 return 1;
114 } else if (str2.startsWith("mgnl:")) {
115 return -1;
116 }
117 int res = String.CASE_INSENSITIVE_ORDER.compare(str1, str2);
118 if (res == 0) {
119 res = str1.compareTo(str2);
120 }
121 return res;
122 }
123 return 0;
124 };
125
126 private static final int DEFAULT_DEPTH = 3;
127
128 private static final ValueFormatterService TYPE_DESCRIPTION_SERVICE = ValueFormatterService.getInstance();
129
130 private Inspector() {
131 }
132
133 public static String dump(Object obj) {
134 return dump(obj, DEFAULT_DEPTH, false);
135 }
136
137 public static String dump(Object obj, int depth) {
138 return dump(obj, depth, false);
139 }
140
141 public static String dump(Object obj, int depth, boolean asHtml) {
142 return doDump(obj, 0, depth, asHtml ? new HtmlOutputter() : new TextOutputter()).asString();
143 }
144
145 private static Outputter doDump(Object obj, int currentDepth, int depth, Outputter outputter) {
146 if (obj == null) {
147 obj = ObjectUtils.NULL;
148 }
149 ValueFormatter type = TYPE_DESCRIPTION_SERVICE.getValueFormatterFor(obj);
150 if (type == null) {
151 type = new ObjectValueFormatter();
152 type.setValue(obj);
153 }
154 if (!type.hasChildren()) {
155 outputter.formatSingleValue(type);
156 } else {
157 if (currentDepth > depth) {
158 outputter.formatMaxDepthReached(type);
159 } else {
160 outputter.formatMultiValue(type);
161 Map<String, Object> children = type.getChildren();
162 for (Map.Entry<String, Object> entry : children.entrySet()) {
163 outputter.indent(currentDepth);
164 outputter.formatKey(entry.getKey());
165 doDump(entry.getValue(), currentDepth + 1, depth, outputter);
166 }
167 }
168 }
169 return outputter;
170 }
171
172
173
174
175
176
177 public static abstract class AbstractValueFormatter<T> implements ValueFormatter<T> {
178
179 private T value;
180
181 public AbstractValueFormatter() {
182 }
183
184 @Override
185 public T getValue() {
186 return value;
187 }
188
189 @Override
190 public void setValue(T value) {
191 this.value = value;
192 }
193
194 @Override
195 public String getName() {
196 return getValue().getClass().getSimpleName();
197 }
198 }
199
200
201
202
203 public static final class NullValueFormatter extends AbstractValueFormatter<ObjectUtils.Null> {
204
205 @Override
206 public String getValueAsString() {
207 return getValue().getClass().getSimpleName().toLowerCase();
208 }
209
210 @Override
211 public String getDescription() {
212 return getValue().getClass().getSimpleName();
213 }
214
215 @Override
216 public boolean canHandle(Class<?> type) {
217 return ObjectUtils.Null.class.equals(type);
218 }
219
220 }
221
222
223
224
225 public static final class BooleanValueFormatter extends AbstractValueFormatter<Boolean> {
226
227 @Override
228 public String getValueAsString() {
229 return String.valueOf(getValue());
230 }
231
232 @Override
233 public String getDescription() {
234 return getValue().getClass().getSimpleName();
235 }
236
237 @Override
238 public boolean canHandle(Class<?> type) {
239 return Boolean.class.isAssignableFrom(type);
240 }
241 }
242
243
244
245
246
247
248 public static final class CharValueFormatter extends AbstractValueFormatter<Character> {
249
250 @Override
251 public String getValueAsString() {
252 return "'" + StringEscapeUtils.escapeHtml4(String.valueOf(getValue())) + "'";
253 }
254
255 @Override
256 public String getDescription() {
257 return getValue().getClass().getSimpleName();
258 }
259
260 @Override
261 public boolean canHandle(Class<?> type) {
262 return Character.class.isAssignableFrom(type);
263 }
264 }
265
266
267
268
269 public static final class NumberValueFormatter extends AbstractValueFormatter<Number> {
270
271 @Override
272 public String getValueAsString() {
273 return String.valueOf(getValue());
274 }
275
276 @Override
277 public String getDescription() {
278 return getValue().getClass().getSimpleName();
279 }
280
281 @Override
282 public String getName() {
283 return Number.class.getSimpleName();
284 }
285
286 @Override
287 public boolean canHandle(Class<?> type) {
288 return Number.class.isAssignableFrom(type);
289 }
290 }
291
292
293
294
295
296
297 public static final class StringValueFormatter extends AbstractValueFormatter<String> {
298
299 @Override
300 public String getValueAsString() {
301 String val = getValue();
302 if (val.length() >= 100) {
303 val = StringUtils.substring(val, 0, 100);
304 val += "...";
305 }
306 return "\"" + StringEscapeUtils.escapeHtml4(val) + "\"";
307 }
308
309 @Override
310 public String getDescription() {
311 return getValue().getClass().getSimpleName();
312 }
313
314 @Override
315 public boolean canHandle(Class<?> type) {
316 return String.class.isAssignableFrom(type);
317 }
318 }
319
320
321
322
323
324
325 public static final class DateValueFormatter extends AbstractValueFormatter<Date> {
326
327 private final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("MMM d, yyyy hh:mm:ss aaa z");
328
329 @Override
330 public String getValueAsString() {
331 return DATE_FORMAT.format(getValue());
332 }
333
334 @Override
335 public String getDescription() {
336 return Date.class.getSimpleName();
337 }
338
339 @Override
340 public boolean canHandle(Class<?> type) {
341 return Date.class.isAssignableFrom(type);
342 }
343 }
344
345
346
347
348
349
350 public static final class CalendarValueFormatter extends AbstractValueFormatter<Calendar> {
351
352 private final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("MMM d, yyyy hh:mm:ss aaa z");
353
354 @Override
355 public String getValueAsString() {
356 return DATE_FORMAT.format(getValue());
357 }
358
359 @Override
360 public String getDescription() {
361 return Calendar.class.getSimpleName();
362 }
363
364 @Override
365 public String getName() {
366 return Calendar.class.getSimpleName();
367 }
368
369 @Override
370 public boolean canHandle(Class<?> type) {
371 return Calendar.class.isAssignableFrom(type);
372 }
373 }
374
375
376
377
378
379
380 public static final class ContentMapValueFormatter extends AbstractValueFormatter<ContentMap> {
381
382 @Override
383 public String getValueAsString() {
384 return getName();
385 }
386
387 @Override
388 public String getDescription() {
389 return String.valueOf(getValue().get("@path"));
390 }
391
392 @Override
393 public boolean hasChildren() {
394 return getValue().size() > 0;
395 }
396
397 @Override
398 public Map<String, Object> getChildren() {
399 Map<String, Object> sorted = new TreeMap<>(KEY_COMPARATOR);
400 for (String key : getValue().keySet()) {
401 sorted.put(key, getValue().get(key));
402 }
403 return sorted;
404 }
405
406 @Override
407 public boolean canHandle(Class<?> type) {
408 return ContentMap.class.equals(type);
409 }
410 }
411
412
413
414
415
416
417
418 public static final class NodeValueFormatter extends AbstractValueFormatter<Node> {
419
420 @Override
421 public String getValueAsString() {
422 return getName();
423 }
424
425 @Override
426 public String getName() {
427 return Node.class.getSimpleName();
428 }
429
430 @Override
431 @SneakyThrows(RepositoryException.class)
432 public String getDescription() {
433 return getValue().getPath();
434 }
435
436 @Override
437 public boolean hasChildren() {
438 return true;
439 }
440
441 @Override
442 @SneakyThrows(RepositoryException.class)
443 public Map<String, Object> getChildren() {
444 Map<String, Object> result = new LinkedHashMap<>();
445 for (Node node : NodeUtil.getNodes(getValue())) {
446 result.put(node.getName(), node);
447 }
448 PropertyIterator propertyIterator = getValue().getProperties();
449 TreeMap<String, Object> sorted = new TreeMap<>(KEY_COMPARATOR);
450 while (propertyIterator.hasNext()) {
451 Property property = propertyIterator.nextProperty();
452 sorted.put(property.getName(), PropertyUtil.getPropertyValueObject(getValue(), property.getName()));
453 }
454 result.putAll(sorted);
455 return result;
456 }
457
458 @Override
459 public boolean canHandle(Class<?> type) {
460 return Node.class.isAssignableFrom(type);
461 }
462 }
463
464
465
466
467
468
469 public static final class PropertyValueFormatter extends AbstractValueFormatter<Property> {
470
471 @Override
472 @SneakyThrows(RepositoryException.class)
473 public String getValueAsString() {
474 if (getValue().isMultiple()) {
475 return getName();
476 } else {
477 return getValue().getString();
478 }
479 }
480
481 @Override
482 @SneakyThrows(RepositoryException.class)
483 public boolean hasChildren() {
484 return getValue().isMultiple();
485 }
486
487 @Override
488 @SneakyThrows(RepositoryException.class)
489 public String getDescription() {
490 return getValue().isMultiple() ? String.valueOf(getValue().getValues().length) : getName();
491 }
492
493 @Override
494 public String getName() {
495 return Property.class.getSimpleName();
496 }
497
498 @Override
499 @SneakyThrows(RepositoryException.class)
500 public Map<String, Object> getChildren() {
501 Map<String, Object> result = new HashMap<>();
502 int i = 0;
503 for (Value v : getValue().getValues()) {
504 result.put(String.valueOf(i++), PropertyUtil.getValueObject(v));
505 }
506 return result;
507 }
508
509 @Override
510 public boolean canHandle(Class<?> type) {
511 return Property.class.isAssignableFrom(type);
512 }
513 }
514
515
516
517
518 public static final class ArrayValueFormatter extends AbstractValueFormatter<Object> {
519
520 @Override
521 public void setValue(Object value) {
522 super.setValue(convertArray(value));
523 }
524
525 @Override
526 public Object[] getValue() {
527 return (Object[]) super.getValue();
528 }
529
530 @Override
531 public String getValueAsString() {
532 return getName();
533 }
534
535 @Override
536 public String getName() {
537 return "Sequence";
538 }
539
540 @Override
541 public String getDescription() {
542 return String.valueOf(getValue().length);
543 }
544
545 @Override
546 public boolean hasChildren() {
547 return getValue().length > 0;
548 }
549
550 @Override
551 public Map<String, Object> getChildren() {
552 int i = 0;
553 Map<String, Object> result = new LinkedHashMap<>();
554 for (Object obj : getValue()) {
555 result.put(String.valueOf(i++), obj);
556 }
557 return result;
558 }
559
560 private static Object[] convertArray(Object objectToDump) {
561 if (objectToDump instanceof Object[]) {
562 return (Object[]) objectToDump;
563 }
564 Object[] result;
565 int length = Array.getLength(objectToDump);
566 result = new Object[length];
567 for (int i = 0; i < length; ++i) {
568 result[i] = Array.get(objectToDump, i);
569 }
570 return result;
571 }
572
573 @Override
574 public boolean canHandle(Class<?> type) {
575 return int[].class.isAssignableFrom(type)
576 || float[].class.isAssignableFrom(type)
577 || double[].class.isAssignableFrom(type)
578 || boolean[].class.isAssignableFrom(type)
579 || byte[].class.isAssignableFrom(type)
580 || short[].class.isAssignableFrom(type)
581 || long[].class.isAssignableFrom(type)
582 || char[].class.isAssignableFrom(type)
583 || Object[].class.isAssignableFrom(type);
584 }
585 }
586
587
588
589
590 public static final class CollectionValueFormatter extends AbstractValueFormatter<Collection<?>> {
591
592 @Override
593 public String getValueAsString() {
594 return getName();
595 }
596
597 @Override
598 public String getDescription() {
599 return String.valueOf(getValue().size());
600 }
601
602 @Override
603 public String getName() {
604 return "Sequence";
605 }
606
607 @Override
608 public boolean hasChildren() {
609 return getValue().size() > 0;
610 }
611
612 @Override
613 public Map<String, Object> getChildren() {
614 int i = 0;
615 Map<String, Object> result = new LinkedHashMap<>();
616 for (Object obj : getValue()) {
617 result.put(String.valueOf(i++), obj);
618 }
619 return result;
620 }
621
622 @Override
623 public boolean canHandle(Class<?> type) {
624 return Collection.class.isAssignableFrom(type);
625 }
626 }
627
628
629
630
631 public static final class MapValueFormatter extends AbstractValueFormatter<Map<Object, Object>> {
632
633 @Override
634 public String getValueAsString() {
635 return getName();
636 }
637
638 @Override
639 public String getDescription() {
640 return String.valueOf(getValue().size());
641 }
642
643 @Override
644 public String getName() {
645 return "Hash";
646 }
647
648 @Override
649 public boolean hasChildren() {
650 return getValue().size() > 0;
651 }
652
653 @Override
654 public Map<String, Object> getChildren() {
655 return getValue().entrySet().stream()
656 .collect(Collectors.toMap(entry -> String.valueOf(entry.getKey()), Map.Entry::getValue));
657 }
658
659 @Override
660 public boolean canHandle(Class<?> type) {
661 return Map.class.isAssignableFrom(type);
662 }
663 }
664
665
666
667
668
669
670 public static final class ObjectValueFormatter extends AbstractValueFormatter<Object> {
671
672 @Override
673 public String getValueAsString() {
674 return getValue().getClass().getSimpleName();
675 }
676
677 @Override
678 public String getDescription() {
679 return "#" + getValue().hashCode();
680 }
681
682 @Override
683 public boolean canHandle(Class<?> type) {
684 return true;
685 }
686 }
687 }