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 public static abstract class AbstractValueFormatter<T> implements ValueFormatter<T> {
176
177 private T value;
178
179 public AbstractValueFormatter() {
180 }
181
182 @Override
183 public T getValue() {
184 return value;
185 }
186
187 @Override
188 public void setValue(T value) {
189 this.value = value;
190 }
191
192 @Override
193 public String getName() {
194 return getValue().getClass().getSimpleName();
195 }
196 }
197
198
199
200
201 public static final class NullValueFormatter extends AbstractValueFormatter<ObjectUtils.Null> {
202
203 @Override
204 public String getValueAsString() {
205 return getValue().getClass().getSimpleName().toLowerCase();
206 }
207
208 @Override
209 public String getDescription() {
210 return getValue().getClass().getSimpleName();
211 }
212
213 @Override
214 public boolean canHandle(Class<?> type) {
215 return ObjectUtils.Null.class.equals(type);
216 }
217
218 }
219
220
221
222
223 public static final class BooleanValueFormatter extends AbstractValueFormatter<Boolean> {
224
225 @Override
226 public String getValueAsString() {
227 return String.valueOf(getValue());
228 }
229
230 @Override
231 public String getDescription() {
232 return getValue().getClass().getSimpleName();
233 }
234
235 @Override
236 public boolean canHandle(Class<?> type) {
237 return Boolean.class.isAssignableFrom(type);
238 }
239 }
240
241
242
243
244
245
246 public static final class CharValueFormatter extends AbstractValueFormatter<Character> {
247
248 @Override
249 public String getValueAsString() {
250 return "'" + StringEscapeUtils.escapeHtml4(String.valueOf(getValue())) + "'";
251 }
252
253 @Override
254 public String getDescription() {
255 return getValue().getClass().getSimpleName();
256 }
257
258 @Override
259 public boolean canHandle(Class<?> type) {
260 return Character.class.isAssignableFrom(type);
261 }
262 }
263
264
265
266
267 public static final class NumberValueFormatter extends AbstractValueFormatter<Number> {
268
269 @Override
270 public String getValueAsString() {
271 return String.valueOf(getValue());
272 }
273
274 @Override
275 public String getDescription() {
276 return getValue().getClass().getSimpleName();
277 }
278
279 @Override
280 public String getName() {
281 return Number.class.getSimpleName();
282 }
283
284 @Override
285 public boolean canHandle(Class<?> type) {
286 return Number.class.isAssignableFrom(type);
287 }
288 }
289
290
291
292
293
294
295 public static final class StringValueFormatter extends AbstractValueFormatter<String> {
296
297 @Override
298 public String getValueAsString() {
299 String val = getValue();
300 if (val.length() >= 100) {
301 val = StringUtils.substring(val, 0, 100);
302 val += "...";
303 }
304 return "\"" + StringEscapeUtils.escapeHtml4(val) + "\"";
305 }
306
307 @Override
308 public String getDescription() {
309 return getValue().getClass().getSimpleName();
310 }
311
312 @Override
313 public boolean canHandle(Class<?> type) {
314 return String.class.isAssignableFrom(type);
315 }
316 }
317
318
319
320
321
322
323 public static final class DateValueFormatter extends AbstractValueFormatter<Date> {
324
325 private final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("MMM d, yyyy hh:mm:ss aaa z");
326
327 @Override
328 public String getValueAsString() {
329 return DATE_FORMAT.format(getValue());
330 }
331
332 @Override
333 public String getDescription() {
334 return Date.class.getSimpleName();
335 }
336
337 @Override
338 public boolean canHandle(Class<?> type) {
339 return Date.class.isAssignableFrom(type);
340 }
341 }
342
343
344
345
346
347
348 public static final class CalendarValueFormatter extends AbstractValueFormatter<Calendar> {
349
350 private final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("MMM d, yyyy hh:mm:ss aaa z");
351
352 @Override
353 public String getValueAsString() {
354 return DATE_FORMAT.format(getValue());
355 }
356
357 @Override
358 public String getDescription() {
359 return Calendar.class.getSimpleName();
360 }
361
362 @Override
363 public String getName() {
364 return Calendar.class.getSimpleName();
365 }
366
367 @Override
368 public boolean canHandle(Class<?> type) {
369 return Calendar.class.isAssignableFrom(type);
370 }
371 }
372
373
374
375
376
377
378 public static final class ContentMapValueFormatter extends AbstractValueFormatter<ContentMap> {
379
380 @Override
381 public String getValueAsString() {
382 return getName();
383 }
384
385 @Override
386 public String getDescription() {
387 return String.valueOf(getValue().get("@path"));
388 }
389
390 @Override
391 public boolean hasChildren() {
392 return getValue().size() > 0;
393 }
394
395 @Override
396 public Map<String, Object> getChildren() {
397 Map<String, Object> sorted = new TreeMap<>(KEY_COMPARATOR);
398 for (String key : getValue().keySet()) {
399 sorted.put(key, getValue().get(key));
400 }
401 return sorted;
402 }
403
404 @Override
405 public boolean canHandle(Class<?> type) {
406 return ContentMap.class.equals(type);
407 }
408 }
409
410
411
412
413
414
415
416 public static final class NodeValueFormatter extends AbstractValueFormatter<Node> {
417
418 @Override
419 public String getValueAsString() {
420 return getName();
421 }
422
423 @Override
424 public String getName() {
425 return Node.class.getSimpleName();
426 }
427
428 @Override
429 @SneakyThrows(RepositoryException.class)
430 public String getDescription() {
431 return getValue().getPath();
432 }
433
434 @Override
435 public boolean hasChildren() {
436 return true;
437 }
438
439 @Override
440 @SneakyThrows(RepositoryException.class)
441 public Map<String, Object> getChildren() {
442 Map<String, Object> result = new LinkedHashMap<>();
443 for (Node node : NodeUtil.getNodes(getValue())) {
444 result.put(node.getName(), node);
445 }
446 PropertyIterator propertyIterator = getValue().getProperties();
447 TreeMap<String, Object> sorted = new TreeMap<>(KEY_COMPARATOR);
448 while (propertyIterator.hasNext()) {
449 Property property = propertyIterator.nextProperty();
450 sorted.put(property.getName(), PropertyUtil.getPropertyValueObject(getValue(), property.getName()));
451 }
452 result.putAll(sorted);
453 return result;
454 }
455
456 @Override
457 public boolean canHandle(Class<?> type) {
458 return Node.class.isAssignableFrom(type);
459 }
460 }
461
462
463
464
465
466
467 public static final class PropertyValueFormatter extends AbstractValueFormatter<Property> {
468
469 @Override
470 @SneakyThrows(RepositoryException.class)
471 public String getValueAsString() {
472 if (getValue().isMultiple()) {
473 return getName();
474 } else {
475 return getValue().getString();
476 }
477 }
478
479 @Override
480 @SneakyThrows(RepositoryException.class)
481 public boolean hasChildren() {
482 return getValue().isMultiple();
483 }
484
485 @Override
486 @SneakyThrows(RepositoryException.class)
487 public String getDescription() {
488 return getValue().isMultiple() ? String.valueOf(getValue().getValues().length) : getName();
489 }
490
491 @Override
492 public String getName() {
493 return Property.class.getSimpleName();
494 }
495
496 @Override
497 @SneakyThrows(RepositoryException.class)
498 public Map<String, Object> getChildren() {
499 Map<String, Object> result = new HashMap<>();
500 int i = 0;
501 for (Value v : getValue().getValues()) {
502 result.put(String.valueOf(i++), PropertyUtil.getValueObject(v));
503 }
504 return result;
505 }
506
507 @Override
508 public boolean canHandle(Class<?> type) {
509 return Property.class.isAssignableFrom(type);
510 }
511 }
512
513
514
515
516 public static final class ArrayValueFormatter extends AbstractValueFormatter<Object> {
517
518 @Override
519 public void setValue(Object value) {
520 super.setValue(convertArray(value));
521 }
522
523 @Override
524 public Object[] getValue() {
525 return (Object[]) super.getValue();
526 }
527
528 @Override
529 public String getValueAsString() {
530 return getName();
531 }
532
533 @Override
534 public String getName() {
535 return "Sequence";
536 }
537
538 @Override
539 public String getDescription() {
540 return String.valueOf(getValue().length);
541 }
542
543 @Override
544 public boolean hasChildren() {
545 return getValue().length > 0;
546 }
547
548 @Override
549 public Map<String, Object> getChildren() {
550 int i = 0;
551 Map<String, Object> result = new LinkedHashMap<>();
552 for (Object obj : getValue()) {
553 result.put(String.valueOf(i++), obj);
554 }
555 return result;
556 }
557
558 private static Object[] convertArray(Object objectToDump) {
559 if (objectToDump instanceof Object[]) {
560 return (Object[]) objectToDump;
561 }
562 Object[] result;
563 int length = Array.getLength(objectToDump);
564 result = new Object[length];
565 for (int i = 0; i < length; ++i) {
566 result[i] = Array.get(objectToDump, i);
567 }
568 return result;
569 }
570
571 @Override
572 public boolean canHandle(Class<?> type) {
573 return int[].class.isAssignableFrom(type)
574 || float[].class.isAssignableFrom(type)
575 || double[].class.isAssignableFrom(type)
576 || boolean[].class.isAssignableFrom(type)
577 || byte[].class.isAssignableFrom(type)
578 || short[].class.isAssignableFrom(type)
579 || long[].class.isAssignableFrom(type)
580 || char[].class.isAssignableFrom(type)
581 || Object[].class.isAssignableFrom(type);
582 }
583 }
584
585
586
587
588 public static final class CollectionValueFormatter extends AbstractValueFormatter<Collection<?>> {
589
590 @Override
591 public String getValueAsString() {
592 return getName();
593 }
594
595 @Override
596 public String getDescription() {
597 return String.valueOf(getValue().size());
598 }
599
600 @Override
601 public String getName() {
602 return "Sequence";
603 }
604
605 @Override
606 public boolean hasChildren() {
607 return getValue().size() > 0;
608 }
609
610 @Override
611 public Map<String, Object> getChildren() {
612 int i = 0;
613 Map<String, Object> result = new LinkedHashMap<>();
614 for (Object obj : getValue()) {
615 result.put(String.valueOf(i++), obj);
616 }
617 return result;
618 }
619
620 @Override
621 public boolean canHandle(Class<?> type) {
622 return Collection.class.isAssignableFrom(type);
623 }
624 }
625
626
627
628
629 public static final class MapValueFormatter extends AbstractValueFormatter<Map<Object, Object>> {
630
631 @Override
632 public String getValueAsString() {
633 return getName();
634 }
635
636 @Override
637 public String getDescription() {
638 return String.valueOf(getValue().size());
639 }
640
641 @Override
642 public String getName() {
643 return "Hash";
644 }
645
646 @Override
647 public boolean hasChildren() {
648 return getValue().size() > 0;
649 }
650
651 @Override
652 public Map<String, Object> getChildren() {
653 return getValue().entrySet().stream()
654 .collect(Collectors.toMap(entry -> String.valueOf(entry.getKey()), Map.Entry::getValue));
655 }
656
657 @Override
658 public boolean canHandle(Class<?> type) {
659 return Map.class.isAssignableFrom(type);
660 }
661 }
662
663
664
665
666
667
668 public static final class ObjectValueFormatter extends AbstractValueFormatter<Object> {
669
670 @Override
671 public String getValueAsString() {
672 return getValue().getClass().getSimpleName();
673 }
674
675 @Override
676 public String getDescription() {
677 return "#" + getValue().hashCode();
678 }
679
680 @Override
681 public boolean canHandle(Class<?> type) {
682 return true;
683 }
684 }
685 }