Ostatnio podczas pracy nad jednym projektem w wielu miejscach w kodzie należało przepisać przychodzące żadanie z jednego transfer object’a na drugiego (powody tegoż przepisywania są dla tego artykułu zupełnie nieistotne) w związku z czym w wielu miejscach w kodzie pojawiły się elementy do siebie łudząco podobne:
target.setSomeProperty(source.getSomeProperty())
Co oczywiście nasunęło mi myśl czy nie dałoby się tego jakoś wydzielić, ale ograniczała mnie myśl, że java to przecież nie taki dajmy na to JavaScript w którym takie figle to chleb powszedni.
Rozwiązanie podsunął mi dopiero Tomek swoim artykułem nt. BeanUtils – wystarczy przecież pobrać własności z jednego beana za pomocą metody describe i następnie korzystając z metody populate je zaaplikować – no i niby wszystko cacy, ale jest parę niuansów które mi przeszkadzają:
- całe api jest statyczne…
- dodawanie Converter‘ów jakioś tak topornie wygląda
- nie ma możliwości definiowania własności, która zmieniła nazwę pomiędzy Beana’mi
- i jak śię jakaś własność nie przepisze, to nie dowiemy się o tym dopóki nie będziemy jej próbowali pobrać, czyli znaaacznie za późno
Stąd pomysł na klasę BeanTransformer, która powinna umożliwiać:
- definiowanie własności ze zmienioną nazwą
- definiowanie własności do pominięcia
- definiowanie Coverter’a z wykorzystaniem generycznego interfejsu, który sam będzie przenosił informację o typie dla jakiego ma być aplikowany
- sprzątanie konwerterów po zakończonej konwersji
I tak właśnie narodziła się klasa BeanConverter, którą omówię poprzez opis jej metod i klas wewnętrznych upraszczających całą zabawę
No to zacznijmy od początku – zadeklarujmy klasę:
public class BeanTransformer {
}
Przydałaby się metoda pozwalająca definiować własności, które nie bedą nam potrzebne i przechowująca je w Set‘cie:
private Set<String> skipProperites = new HashSet<String>();
public BeanTransformer skip(String firstProperty, String... propertyNames) {
skipProperites.add(firstProperty);
if (propertyNames != null) {
skipProperites.addAll(Arrays.asList(propertyNames));
}
return this;
}
Jak widać przy okazji umożliwia chain’y i dodawanie więcej niż jednej własności za jednym zamachem.
Następnie potrzebujemy metodę pozwalającą zdefiniować własności, których nazwa się zmieniła:
private Map<String, String> renameProperties = new HashMap<String, String>();
public BeanTransformer rename(String fromProperty, String toProperty) {
renameProperties.put(fromProperty, toProperty);
return this;
}
I teraz wystarczy przejechać się po własnościach, sprawdzić które na które przechodza, usunąć zbędne i sprawdzić czy coś zostało i ew. zgłosić błąd.
private Set<String> filterCorrectProperties(final Map<String, Object> properties) {
// first remove properties that should be skipped
properties.keySet().removeAll(skipProperites);
// than go over the rest of them
Set<String> foundProperties = Sets.filter(renameProperties.keySet(),
new Predicate<String>() {
public boolean apply(String fromProperty) {
if (properties.containsKey(fromProperty)) {
Object v = properties.remove(fromProperty);
String newKey = renameProperties.get(fromProperty);
properties.put(newKey, v);
return false;
}
return true;
}
});
return foundProperties;
}
Teraz gdy już mamy listę przefiltrowanych własności możemy je przepisać do docelowego beana
@SuppressWarnings("unchecked")
public <F, T> T transform(F fromBean, T toBean) throws NotUsedPropertiesException {
try {
Map<String, Object> properties = (Map<String, Object>) PropertyUtils.describe(fromBean);
Set<String> foundProperties = filterCorrectProperties(properties);
if (!foundProperties.isEmpty()) {
throw new NotUsedPropertiesException(foundProperties);
}
BeanUtils.populate(toBean, properties);
return toBean;
} catch (IllegalAccessException e) {
throw new NotUsedPropertiesException(e);
} catch (InvocationTargetException e) {
throw new NotUsedPropertiesException(e);
} catch (NoSuchMethodException e) {
throw new NotUsedPropertiesException(e);
}
}
Zakładamy, że reszta własności przepisuje się 1-1.
Jeszcze miłą opcją byłaby możliwość dodania własnego converter’a dla typów innych niż standardowe – tutaj skorzystam również z klasy szablonowej, która będzie rozszerzać interfejs Converter, ale dodatkowo definicja zawiera typ klasy który dany konwerter obsługuje, co daje nam 2 zalety:
- Klasa niesie cały zasób informacji nt. konwersji
- Część wspólnej logiki może być wydzielona na zewnątrz
Zacznijmy od samego interfejsu, który będzie prosty do bólu:
public static interface Converter<T> extends org.apache.commons.beanutils.Converter {}
Teraz metoda rejestrująca takiego konwerterka nie musi mieć explicite podanej klasy, pobierze ją sobie z szablonu korzystając z metody getGenericInterfaces:
public <T> BeanTransformer addConverter(Converter<T> c, boolean skipConverterImpl) {
Type[] types = c.getClass().getGenericInterfaces();
Class<T> clz = (Class<T>) ((ParameterizedType) types[0]).getActualTypeArguments()[0];
if (!skipConverterImpl) {
c = new ConverterImpl(clz, c);
}
converters.put(clz, c);
return this;
}
Jeszcze tylko pozostaje wyjaśnić znaczenie tajemniczej klasy ConverterImp – ona to mianowicie jest sposobem na wydzielenie części wspólnej funkcjonalności dla wszystkich converter’ów a jednocześnie nie wymusza na użytkowniku jej znajomości, wiec po prostu dekoruje metodę convert i dopiero dopuszcza do głosu konkretna implementację, jeżeli wspólna implementacja nie wie jak dany obiekt utworzyć, no ale dośc gadania:
private static class ConverterImpl<T> implements Converter<T> {
private Class<T> self;
private Converter<T> target;
public ConverterImpl(Class<T> self, Converter<T> target) {
this.self = self;
this.target = target;
}
@SuppressWarnings("unchecked")
public Object convert(Class arg0, Object arg1) {
/*
corrected after comment by bob
if (self.isAssignableFrom(arg1.getClass())) {
*/
if (self.isInstance(arg1)) {
return arg1;
}
return target.convert(arg0, arg1);
}
}
Mając convertery możemy je sobie włączać i wyłączac przed i po konwersji:
private void registerConverters() {
for (Class<?> clz : converters.keySet()) {
ConvertUtils.register(converters.get(clz), clz);
}
}
private void deregisterConverters() {
for (Class<?> clz : converters.keySet()) {
ConvertUtils.deregister(clz);
}
}
// w metodzie transform dodajemy wywołania:
registerConverters();
BeanUtils.populate(toBean, properties);
deregisterConverters();
Dla tych, których zaciekawiłem tym wpisem wrzuciłem projekt (wraz z unitami!) na githuba.