View Javadoc
1   /**
2    * This file Copyright (c) 2017-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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   * Inspector is a utility that dumps objects into readable format.
72   *
73   * <p>Inspector is able to print objects in plain text and HTML format by default. No other formats are now supported.</p>
74   * <p>Inspector comes with default implementations of {@link ValueFormatter}s that can handle the following objects:</p>
75   * <ul>
76   * <li>Null values</li>
77   * <li>Primitives</li>
78   * <li>{@link String}s</li>
79   * <li>Dates ({@link Date} and {@link Calendar})</li>
80   * <li>{@link ContentMap}s</li>
81   * <li>{@link Node}s</li>
82   * <li>{@link Property}s</li>
83   * <li>Arrays</li>
84   * <li>{@link Collection}s</li>
85   * <li>{@link Map}s</li>
86   * </ul>
87   * <p>Inspector uses <a href="https://docs.oracle.com/javase/tutorial/ext/basics/spi.html">Service Provider Interface</a>
88   * pattern for handling registered {@link ValueFormatter}s.</p>
89   * <p>Developers may register custom {@link ValueFormatter} implementations the following way:</p>
90   * <ul>
91   *     <li>Create text file {@code info.magnolia.templating.inspector.spi.ValueFormatter$Definition} under {@code META-INF/services} directory.</li>
92   *     <li>Open the file and add custom {@link ValueFormatter} implementation.</li>
93   * </ul>
94   *
95   * @see ValueFormatter
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         // sort only string based keys
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      * Abstract implementation of the {@link ValueFormatter}.
174      *
175      * @param <T> object type
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      * Type description for {@code null} values.
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      * {@link Boolean} type description.
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      * {@link Character} type description.
245      *
246      * <p>All characters are escaped and wrapped by single quotes.</p>
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      * {@link Number} type description.
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      * {@link String} type description.
294      *
295      * <p>Strings longer than 100 characters are trimmed, all strings are escaped and wrapped by double quotes.</p>
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      * {@link Date} type description.
322      *
323      * <p>Date is formatted using {@code MMM d, yyyy hh:mm:ss aaa z} pattern, timezone corresponds with the server timezone.</p>
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      * {@link Calendar} type description.
347      *
348      * <p>Calendar date is formatted using {@code MMM d, yyyy hh:mm:ss aaa z} pattern, timezone corresponds with the server timezone.</p>
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      * {@link ContentMap} type description.
377      *
378      * <p>Values of the {@link ContentMap} are sorted alphabetically, values prefixed with {@code mgnl:} and {@code jcr:} are sorted at the end.</p>
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      * {@link Node} type description.
414      *
415      * <p>Both sub-nodes and properties of the corresponding node are returned as children.
416      * Properties are sorted alphabetically, properties prefixed with {@code mgnl:} and {@code jcr:} are sorted at the end.</p>
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      * {@link Property} type description.
466      *
467      * <p>Both single and multi valued properties are handled by {@link PropertyValueFormatter}.</p>
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      * Array type description.
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      * {@link Collection} type description.
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      * {@Link Map} type description.
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      * {@link Object} type description.
667      *
668      * <p>Note: object properties are not introspected.</p>
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 }