wtorek, stycznia 31, 2017

Niecne wykorzystanie refleksji... czyli jak poszukać tekstu w drzewie obiektów? ;-)

Pomysł nie jest mój, a kolegi z pracy.
I jego pomysł jest zdecydowanie bardziej rozbudowany, ja tylko postanowiłem sprawdzić czy pomysł jest realizowalny ;-)
I chyba jest.

O co chodzi?

Często w trakcie debugowania kodu dochodzi się do momentu, że wiadomo że jakaś rzecz, zwykle tekst jest gdzieś tam w drzewie obiektów, ale może być tak głęboko, że nie ma szans tam dotrzeć...

Przydałby się automat, który potrafiłby to zrobić ;-)

To jest ten automat ;-)

package pl.przemelek.challenges;

import java.lang.reflect.Field;
import java.security.ProtectionDomain;
import java.util.*;

public class Inspector {
private Set visited = new HashSet();
private Inspector() {}

public static String inspect(Object root, String text) throws IllegalAccessException {
List<String> strings = new Inspector()._inspect(root, text);
StringBuilder sb = new StringBuilder();
for (String s:strings) {
sb.append("x"+s).append("\n");
}
return sb.toString();
}

private Object getValue(Field field, Object instance) throws IllegalAccessException {
boolean acc = field.isAccessible();
field.setAccessible(true);
Object obj = field.get(instance);
field.setAccessible(acc);
return obj;
}

private List<String> _inspect(Object root, String text) throws IllegalAccessException {
List<String> found = new ArrayList<>();
if (root!=null && !visited.contains(root) && (!ProtectionDomain.class.isAssignableFrom(root.getClass()))) {
visited.add(root);
if (root.getClass().equals(String.class)) {
String str = (String)root;
if (str.indexOf(text)!=-1) {
found.add("="+str);
}
} else {
Field[] _fields = root.getClass().getFields();
Field[] _declaredFields = root.getClass().getDeclaredFields();
Set<Field> fields = new HashSet<>();
fields.addAll(Arrays.asList(_fields));
fields.addAll(Arrays.asList(_declaredFields));
for (Field field:fields) {
Object val = getValue(field,root);
if (val==null) continue;
if (CharSequence.class.isAssignableFrom(val.getClass())) {
String str = ((CharSequence)getValue(field, root)).toString();
if (str!=null && str.indexOf(text)!=-1) {
found.add("."+field.getName()+"="+str);
}
} else if (Iterable.class.isAssignableFrom(val.getClass())) {
Iterable iterable = (Iterable)getValue(field, root);
List<String> l = new ArrayList<>();
iterable.forEach(x -> {
try {
l.addAll(_inspect(x, text));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
for (String s:l) {
found.add("."+field.getName()+" contains "+text);
}
} else if (Map.class.isAssignableFrom(val.getClass())) {
Map map = (Map)getValue(field, root);
List<String> l = new ArrayList<>();
map.entrySet().forEach(x -> {
try {
l.addAll(_inspect(x, text));
} catch (IllegalAccessException e) {
e.printStackTrace();
}

});
for (String s:l) {
found.add("."+field.getName()+" contains "+text);
}
} else {
Object obj = getValue(field,root);
List<String> l = _inspect(obj,text);
for (String s:l) {
found.add("."+field.getName()+s);
}
}
}
}
}
return found;
}
}

Jak to działa?

Klasa ta musi się znajdować na naszym classpath'cie.
Zakładamy break point w jakimś miejscu w kodzie.
Gdy kod się zatrzyma używamy w IntelliJ (w innych też musi być jakiś odpowiednik tego) Evaluate Expression i wpisujemy do ewaluacji wyrażenie Inspector.inspect(obiektKtóryChemyPrzeszukać,"tekst którego szukamy") (możliwe, że trzeba będzie pl.przemelek.challenges.Inspector.inspect(obiekt,"tekst")), tak jak na obrazku poniżej:


Rezultatem jest tekst, który wypisuje wszystkie "ścieżki" do String'ów (a dokładniej CharSequence'ów) zawierających szukane słowo.

Ja wszystko testowałem na:

package pl.przemelek.challenges;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class InspectorTest {

static class Wrapper {
private String test2 = "toster test tostowy";
}

private String test = "toster test tostowy";
private Wrapper wrapper = new Wrapper();
private Map<String, String> map = new HashMap<>();
private List<String> list = new ArrayList<>();


public static void main(String[] args) throws IllegalAccessException {
InspectorTest it = new InspectorTest();
it.map.put("test","toster");
it.map.put("toster","test");
it.list.add("Toster");
it.list.add("test");
System.out.println(it.test);
}
}

Zakładając breakpoint na ostatniej linii metody main.

Zobaczę czy to dołączy do mojego prywatnego zestawu sztuczek przy debugowaniu ;-)
Bo jak znam życie jest masa miejsc gdzie się wyłoży ;-)

Teraz o tym jak to ogólnie działa.
Wychodzi po pierwsze z założenia, że z obiektu wychodzi nam drzewo (OK, wychodzi graf, ale drzewo to taki specjalny rodzaj grafu i na wszystkich powinien działać DFS, BFS byłby bezpieczniejszy, ale chodziło o proof of concept ;-), by nie wpaść w pętle używamy Set'u do trzymania odwiedzonych już obiektów... tak naprawdę z dokładnością do hashCode i equals).
Zaczynamy od góry drzewa obiektu i przechodzimy przez jego wszystkie pola, jeśli są CharSequence'ami to sprawdzamy ich zawartość, jeśli nie są to idziemy głębiej.
W końcu zwracamy listę wszystkich znalezionych pól ;-)


Podobne postybeta
Sztuczki tropiciela błędów, part 4
Sztuczki tropiciela błędów, part 2 ;-)
Sztuczki tropiciela błędów, part 3 - hackujemy klasy finalne ;-)
Monitorujemy cenę IntelliJ'a ;-)
Przepływ sterowany danymi - A takie Java'owe coś ;-)