001    /*
002     * $Id: ObjectInspector.java,v 1.10 2013/12/19 21:53:57 oboehm Exp $
003     *
004     * Copyright (c) 2012 by Oliver Boehm
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     *
018     * (c)reated 24.01.2012 by oliver (ob@oasd.de)
019     */
020    
021    package patterntesting.runtime.util;
022    
023    import java.io.*;
024    import java.lang.reflect.Field;
025    import java.util.*;
026    import java.util.regex.Pattern;
027    
028    import org.slf4j.*;
029    
030    /**
031     * This class lets you examine an object and dump its internal attributes.
032     * It was introduced to find some secrets of IBM's classloader.
033     *
034     * @author oboehm
035     * @since 1.2.10-YEARS (24.01.2012)
036     */
037    public final class ObjectInspector {
038    
039        /** The value for "nothing found". */
040        public static final String NOTHING_FOUND = null;
041    
042        private static final Logger log = LoggerFactory.getLogger(ObjectInspector.class);
043        private final Object inspected;
044        private final Set<Object> visited = new HashSet<Object>();
045    
046        /**
047         * Instantiates a new object inspector.
048         *
049         * @param obj the object to be inspected
050         */
051        public ObjectInspector(final Object obj) {
052            this.inspected = obj;
053        }
054    
055        /**
056         * Gets the type (class) of the stored object.
057         *
058         * @return the type
059         */
060        public Class<?> getType() {
061            return this.inspected.getClass();
062        }
063    
064        /**
065         * Find the value in of the attributes of the inspected object. You can
066         * give a {@link Pattern} as parameter if you want to use wildcards for
067         * the search.
068         *
069         * @param value the value or the pattern you want to look for
070         * @return the string
071         * @throws ValueNotFoundException the value not found exception
072         */
073        public String findValue(final Object value) throws ValueNotFoundException {
074            try {
075                return findValue(value, this.inspected, this.inspected.getClass().getName());
076            } finally {
077                this.visited.clear();
078            }
079        }
080    
081        private String findValue(final Object value, final Object where, final String path)
082                throws ValueNotFoundException {
083            for (Field field : getAllFields(where.getClass())) {
084                field.setAccessible(true);
085                try {
086                    Object obj = field.get(where);
087                    String fieldPath = path + "." + field.getName();
088                    if (isEquals(obj, value)) {
089                        return fieldPath;
090                    }
091                    if (alreadyVisited(obj)) {
092                        continue;
093                    }
094                    if (isArrayType(obj)) {
095                        return findArrayValue(value, obj, fieldPath);
096                    } else if (isIterable(obj)) {
097                        return findValue(value, getIterator(obj), fieldPath);
098                    } else if (isComplexType(obj)) {
099                        try {
100                            return findValue(value, obj, fieldPath);
101                        } catch (ValueNotFoundException vnfe) {
102                            log.trace("value not found in {}.{}", path, field);
103                        }
104                    }
105                } catch (IllegalAccessException e) {
106                    throw new IllegalArgumentException("can't access " + field + " in "
107                            + where.getClass());
108                }
109            }
110            throw new ValueNotFoundException(value);
111        }
112    
113        private String findArrayValue(final Object value, final Object where, final String path)
114                throws ValueNotFoundException {
115            try {
116                Object[] objects = (Object[]) where;
117                for (int i = 0; i < objects.length; i++)  {
118                    String arrayPath = path + "[" + i + "]";
119                    if (isEquals(value, objects[i])) {
120                        return arrayPath;
121                    }
122                    if (objects[i] == null) {
123                        continue;
124                    }
125                    try {
126                        return findValue(value, objects[i], path + ".");
127                    } catch (ValueNotFoundException vnfe) {
128                        log.trace("value not found in {}.", arrayPath);
129                    }
130                }
131            } catch (ClassCastException cce) {
132                log.trace("Do not look in native array {} for {}.", where, value);
133            }
134            throw new ValueNotFoundException(value);
135        }
136    
137        private String findValue(final Object value, final Iterator<?> where, final String path)
138                throws ValueNotFoundException {
139            int i = 0;
140            while (where.hasNext()) {
141                Object obj = where.next();
142                String arrayPath = path + "[" + i + "]";
143                if (isEquals(value, obj)) {
144                    return arrayPath;
145                }
146                try {
147                    return findValue(value, obj, arrayPath);
148                } catch (ValueNotFoundException vnfe) {
149                    log.trace("value not found in {}.", arrayPath);
150                }
151            }
152            throw new ValueNotFoundException(value);
153        }
154    
155        private static boolean isEquals(final Object one, final Object two) {
156            if (one == null) {
157                return two == null;
158            }
159            if (two == null) {
160                return false;
161            }
162            if (one.equals(two)) {
163                return true;
164            }
165            try {
166                Pattern pattern = (Pattern) one;
167                return pattern.matcher(two.toString()).matches();
168            } catch (ClassCastException cce) {
169                return false;
170            }
171        }
172    
173        /**
174         * Gets all fields of the wrapped object. Not only the (public) class
175         * fields but also the private and protected fields of the superclass.
176         *
177         * @return the all fields
178         */
179        public Collection<Field> getAllFields() {
180            return getAllFields(this.inspected.getClass());
181        }
182    
183        private static Collection<Field> getAllFields(final Class<?> clazz) {
184            Collection<Field> fields = new ArrayList<Field>();
185            addFields(fields, clazz);
186            return fields;
187        }
188    
189        private static void addFields(final Collection<Field> fields, final Class<?> clazz) {
190            Class<?> superclass = clazz.getSuperclass();
191            if (superclass != null) {
192                addFields(fields, superclass);
193            }
194            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
195        }
196    
197        /**
198         * Dump the inspected class.
199         *
200         * @param writer the writer
201         * @throws IOException Signals that an I/O exception has occurred.
202         */
203        public synchronized void dump(final Writer writer) throws IOException {
204            writer.append("=== Dump of " + this.inspected.getClass() + " ===\n");
205            dump(writer, this.inspected, this.inspected.getClass().getName());
206            this.visited.clear();
207        }
208    
209        private void dump(final Writer writer, final Object obj, final String prefix)
210                throws IOException {
211            if (obj == null) {
212                writer.append(prefix + "(null)\n");
213                return;
214            }
215            Collection<Field> fields = getAllFields(obj.getClass());
216            for (Field field : fields) {
217                dump(writer, field, obj, prefix);
218                try {
219                    Object value = field.get(obj);
220                    if ((value == null) || alreadyVisited(value)) {
221                        continue;
222                    }
223                    if (isArrayType(value)) {
224                        dumpArray(writer, value, prefix);
225                    } else if (isIterable(value)) {
226                        try {
227                            Iterator<?> iterator = getIterator(value);
228                            int i = 0;
229                            while (iterator.hasNext()) {
230                                Object next = iterator.next();
231                                dump(writer, next, prefix + "[" + i + "]");
232                                i++;
233                            }
234                        } catch (ConcurrentModificationException cme) {
235                            log.warn("Houston, we have a problem with iterator of " + value, cme);
236                            writer.append(prefix + "[..]");
237                            writer.append(" = ??? (" + cme + ")\n");
238                            ThreadUtil.sleep();
239                        }
240                    } else if (isComplexType(value)) {
241                        dump(writer, value, prefix + "." + field.getName());
242                    }
243                } catch (IllegalAccessException iae) {
244                    throw new IllegalArgumentException("can't access " + field, iae);
245                }
246            }
247        }
248    
249        private static void dump(final Writer writer, final Field field, final Object obj,
250                final String prefix) throws IOException {
251            field.setAccessible(true);
252            writer.append(prefix);
253            writer.append('.');
254            writer.append(field.getName());
255            writer.append(" = (");
256            writer.append(field.getType().getSimpleName());
257            writer.append(") ");
258            try {
259                writer.append(Converter.toString(field.get(obj)));
260            } catch (IllegalAccessException iae) {
261                log.debug("can't access field {}", field, iae);
262                writer.append("??? (");
263                writer.append(iae.getLocalizedMessage());
264                writer.append(")");
265            }
266            writer.append("\n");
267        }
268    
269        private void dumpArray(final Writer writer, final Object value, final String prefix)
270                throws IOException {
271            try {
272                Object[] array = (Object[]) value;
273                for (int i = 0; i < array.length; i++) {
274                    dump(writer, array[i], prefix + "[" + i + "]");
275                }
276            } catch (ClassCastException cce) {
277                log.trace("Native array {} is not dumped with each element.", value);
278            }
279        }
280    
281        /**
282         * Checks if the wrapped object is complex type.
283         *
284         * @return true, if is complex type
285         */
286        public boolean isComplexType() {
287            return isComplexType(this.inspected);
288        }
289    
290        /**
291         * Checks if the given object is of complex type. These are all types which
292         * <ul>
293         *  <li>are not a primitive type (like int, char, ...)</li>
294         *  <li>are not a String class</li>
295         *  <li>are not of subtype Numer (like Long, Short, ...)</li>
296         * </ul>
297         *
298         * @param obj the obj
299         * @return true, if is complex type
300         */
301        public static boolean isComplexType(final Object obj) {
302            if (obj == null) {
303                return false;
304            }
305            Class<?> clazz = obj.getClass();
306            if (clazz.isPrimitive()) {
307                return false;
308            }
309            if (String.class.equals(clazz)) {
310                return false;
311            }
312            if (Number.class.isAssignableFrom(clazz)) {
313                return false;
314            }
315            if (Character.class.isAssignableFrom(clazz)) {
316                return false;
317            }
318            return true;
319        }
320    
321        /**
322         * Checks if the wrapped object is an array type.
323         *
324         * @return true, if is array type
325         */
326        public boolean isArrayType() {
327            return isArrayType(this.inspected);
328        }
329    
330        /**
331         * Checks if the given object is an array.
332         *
333         * @param obj the obj
334         * @return true, if is array type
335         */
336        public static boolean isArrayType(final Object obj) {
337            if (obj == null) {
338                return false;
339            }
340            Class<?> clazz = obj.getClass();
341            return clazz.isArray();
342        }
343    
344        /**
345         * Checks if the type of the wrapped object could be iterated. This is the
346         * case e.g. for Collections and its subclasses.
347         *
348         * @return true, if is iterable
349         */
350        public boolean isIterable() {
351            return isIterable(this.inspected);
352        }
353    
354        /**
355         * Checks if the type of the given object could be iterated. This is the
356         * case e.g. for Collections and its subclasses.
357         *
358         * @param obj the object
359         * @return true, if is iterable
360         */
361        public static boolean isIterable(final Object obj) {
362            if (obj == null) {
363                return false;
364            }
365            Class<?> clazz = obj.getClass();
366            try {
367                clazz.getMethod("iterator");
368                return true;
369            } catch (SecurityException e) {
370                throw new IllegalArgumentException("can't access methods for " + clazz, e);
371            } catch (NoSuchMethodException e) {
372                return false;
373            }
374        }
375    
376        private static Iterator<?> getIterator(final Object value) {
377            return (Iterator<?>) ReflectionHelper.invokeMethod(value, "iterator");
378        }
379    
380        private boolean alreadyVisited(final Object value) {
381            try {
382                if (visited.contains(value)) {
383                    return true;
384                }
385                visited.add(value);
386            } catch (RuntimeException ex) {
387                log.debug("can't store \"{}\"", value, ex);
388            }
389            return false;
390        }
391    
392        /**
393         * Dumps all attributes of the inspected object in the form
394         * <code>attribute=value</code>.
395         *
396         * @return the string
397         * @see java.lang.Object#toString()
398         */
399        @Override
400        public String toString() {
401            return this.getClass().getSimpleName() + " for " + this.inspected.getClass();
402        }
403    
404        /**
405         * To long string.
406         *
407         * @return the string
408         */
409        public String toLongString() {
410            StringBuilder buffer = new StringBuilder();
411            Class<?> clazz = this.inspected.getClass();
412            while(clazz != null) {
413                buffer.insert(0, toString(clazz.getDeclaredFields()));
414                clazz = clazz.getSuperclass();
415            }
416            return buffer.toString();
417        }
418    
419        private String toString(final Field[] fields) {
420            return toString(fields, this.inspected);
421        }
422    
423        private static String toString(final Field[] fields, final Object obj) {
424            StringWriter buffer = new StringWriter();
425            try {
426                for (int i = 0; i < fields.length; i++) {
427                    dump(buffer, fields[i], obj, "");
428                }
429                buffer.close();
430            } catch (IOException canthappen) {
431                log.info("I have some problems dumping fields {}.", fields, canthappen);
432            }
433            return buffer.toString();
434        }
435    
436    }
437