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     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      * Type description for {@code null} values.
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      * {@link Boolean} type description.
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      * {@link Character} type description.
243      *
244      * <p>All characters are escaped and wrapped by single quotes.</p>
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      * {@link Number} type description.
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      * {@link String} type description.
292      *
293      * <p>Strings longer than 100 characters are trimmed, all strings are escaped and wrapped by double quotes.</p>
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      * {@link Date} type description.
320      *
321      * <p>Date is formatted using {@code MMM d, yyyy hh:mm:ss aaa z} pattern, timezone corresponds with the server timezone.</p>
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      * {@link Calendar} type description.
345      *
346      * <p>Calendar date is formatted using {@code MMM d, yyyy hh:mm:ss aaa z} pattern, timezone corresponds with the server timezone.</p>
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      * {@link ContentMap} type description.
375      *
376      * <p>Values of the {@link ContentMap} are sorted alphabetically, values prefixed with {@code mgnl:} and {@code jcr:} are sorted at the end.</p>
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      * {@link Node} type description.
412      *
413      * <p>Both sub-nodes and properties of the corresponding node are returned as children.
414      * Properties are sorted alphabetically, properties prefixed with {@code mgnl:} and {@code jcr:} are sorted at the end.</p>
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      * {@link Property} type description.
464      *
465      * <p>Both single and multi valued properties are handled by {@link PropertyValueFormatter}.</p>
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      * Array type description.
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      * {@link Collection} type description.
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      * {@Link Map} type description.
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      * {@link Object} type description.
665      *
666      * <p>Note: object properties are not introspected.</p>
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 }