Obsługa DTO dla Spring i SmartGWT

, , niedziela, 31 lipca 2011

Ostatnio miałem problem z przekazywaniem informacji pomiędzy częścią serwerową aplikacji opartą o serwisy w postaci bean-ów Spring oraz częścią kliencką przygotowaną w formie aplikacji SmartGWT. Problem polegał na tym, że miałem dosyć dużo obiektów domenowych JPA i żeby wymieniać informacje z aplikacją kliencką przy standardowym podejściu musiałbym napisać dla każdej klasy obiektu domenowego klasę DTO. Stwierdziłem, że to strasznie dużo roboty i zacząłem się zastanawiać jak sobie ułatwić życie i zrobić to jakimś automatem. Wpadłem na pomysł przesyłania informacji w postaci tzw. dynamic property i zrobiłem pod to implementację, którą chciałbym się w tym momencie podzielić.

Pomysł polega na tym, aby zarówno po stronie serwerowej jak i klienckiej napisać serializer i deserailzer dynamic property. Rozpocząłem od napisania adnotacji, która ma zasięg metody i służyć będzie do oznaczenie getterów dla property przesyłanych na stronę serwerową i na odwrót. Adnotacja taka ma następującą postać:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicDataProperty {

boolean embeded() default false;
}


No dobrze mamy adnotację to teraz oznaczmy przy jej użyciu odpowiednie obiekty domenowe. Załóżmy, że mamy obiekt domenowy Customer, który wykorzystuje zanurzony obiekt Address tak jak poniżej.

@javax.persistence.Entity
@Table(name = "customers")
public class Customer extends EntityObject {
private static final long serialVersionUID = -5104067194504977189L;

@Column(name = "code", unique = true, nullable = false, length = 60)
private String code;

@Column(name = "name", unique = false, nullable = false, length = 255)
private String name;

@Column(name = "discount", unique = false, nullable = false)
private short discount;

@Embedded
private Address address = new Address();

@DynamicDataProperty
public String getCode() {
return this.code;
}

public void setCode(String code) {
this.code = code;
}

@DynamicDataProperty
public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

@DynamicDataProperty
public short getDiscount() {
return this.discount;
}

public void setDiscount(short discount) {
this.discount = discount;
}

@DynamicDataProperty(embeded = true)
public Address getAddress() {
return this.address;
}

}



@javax.persistence.Embeddable
public class Address implements Serializable {
private static final long serialVersionUID = -2023844913320728453L;

@Column(name = "city", length = 60)
private String city;

@Column(name = "postal_code", length = 10)
private String postalCode;

@Column(name = "street")
private String street;

@Column(name = "local", length = 10)
private String local;

public String getCityPart() {
StringBuffer bf = new StringBuffer();
if (this.postalCode != null) {
bf.append(this.postalCode);
bf.append(" ");
}
if (this.city != null) {
bf.append(this.city);
}
return bf.toString();
}

public String getStreetPart() {
StringBuffer bf = new StringBuffer();
if (this.street != null) {
bf.append(this.street);
}
if (this.local != null) {
bf.append(" ");
bf.append(this.local);
}
return bf.toString();
}

@DynamicDataProperty
public String getFull() {
StringBuffer bf = new StringBuffer();
bf.append(getStreetPart());
if (bf.length() > 0) {
bf.append(", ");
}
bf.append(getCityPart());
return bf.toString();
}

@DynamicDataProperty
public String getCity() {
return this.city;
}

public void setCity(String city) {
this.city = city;
}

@DynamicDataProperty
public String getPostalCode() {
return this.postalCode;
}

public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}

@DynamicDataProperty
public String getStreet() {
return this.street;
}

public void setStreet(String street) {
this.street = street;
}

@DynamicDataProperty
public String getLocal() {
return this.local;
}

public void setLocal(String local) {
this.local = local;
}

}



Jak widać wszystkie gettery zarówno w klasie Customer jak i Address zostały oznaczone adnotacją co powoduje, że wszystkie propery będą przekazywane pomiędzy częścią serwerową i kliencką aplikacji. Dodatkowo getAddress() został oznaczony adnotacją z ustawieniem embeded = true co oznacza, że property dotyczy obiektu zanurzonego, lub inaczej mówiąc obiektu którego serializacja i deserializacja powinna dotyczyć w sensie serializacji jego property.

Teraz tworzymy dwie klasy obiekty których będą przesyłane pomiędzy częścią serwerową i kliencką. Są to klasy do przesyłania zserializowanej postaci obiektów domenowych, czyli swojego rodzaju generyczne DTO. Tak więc mamy klasę konetenerową:

public class DynamicData extends AbstractData {
private static final long serialVersionUID = -2549646020563019965L;

private LinkedHashMap map = new LinkedHashMap();

public LinkedHashMap getMap() {
return this.map;
}

@Override
public Long getId() {
DataProperty prop = this.map.get("id");
return (Long) (prop != null ? prop.getValue() : null);
}
}


i klasę komponentową:

public class DataProperty implements Serializable {
private static final long serialVersionUID = -4627984997682982978L;

private DataPropertyType type = DataPropertyType.STRING;
private String str;
private Integer integer;
private Short shortv;
private Long longv;
private Boolean boolv;
private DynamicData embebed;

public DataProperty() {
this("");
}

public DataProperty(Object value) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null");
}
if (value instanceof String) {
this.str = (String) value;
} else if ((value instanceof DynamicData)) {
this.embebed = (DynamicData) value;
this.type = DataPropertyType.EMBEDED;
} else if ((value instanceof Boolean)) {
this.boolv = (Boolean) value;
this.type = DataPropertyType.BOOL;
} else if ((value instanceof Short)) {
this.shortv = ((Short) value);
this.type = DataPropertyType.SHORT;
} else if ((value instanceof Integer)) {
this.integer = (Integer) value;
this.type = DataPropertyType.INT;
} else if (value instanceof Long) {
this.longv = (Long) value;
this.type = DataPropertyType.LONG;
} else {
this.str = value.toString();
}
}

public Object getValue() {
switch (this.type) {
case EMBEDED:
return this.embebed;
case BOOL:
return this.boolv;
case SHORT:
return this.shortv;
case INT:
return this.integer;
case LONG:
return this.longv;
default:
return this.str;
}
}

public DataPropertyType getType() {
return this.type;
}
}


public enum DataPropertyType implements Serializable {

STRING,
INT,
LONG,
BOOL,
EMBEDED,
SHORT;
}



Jak widać w klasie komponentowej każdy typ ma swoje pole. Można by zapytać dlaczego nie zrobić jednego pola typu Object, w którym zapisana była by informacja o zserializowanej wartości. Rzeczywiście tak by było najprościej, ale niestety jest to problematyczne z racji wykorzystania GWT. Ponieważ obiekty komponentowe będą przesyłane na stronę kliencką muszą być skompilowane przez GWT do javasrcipt-u i jeżeli damy Obiect kompilator będzie próbował stworzyć generyczny javascript i tu dostaniemy albo warningi przy kompilacji albo problemy przy serializacji w runtime. Dlatego najlepiej jest wyręczyć kompilator GWT i samemu przygotować implementację opierająca się o jasno określone typy serializowanych wartości.

No dobrze mamy klasy domenowe, mamy komponenty umożliwiające nam przekazywanie informacji pomiędzy częścią serwerową i kliencką teraz potrzebujemy serializatorów. Serializator po stronie klienckiej ma następującą postać:

public class DynamicDataMapper {
private static final Logger log = LoggerFactory.getLogger(DynamicDataMapper.class);

private DynamicDataMapper() {
}

public static void map(DynamicData data, T entity) {
if ((data != null) && (entity != null)) {
try {
to(data, entity);
} catch (Exception e) {
throw new MappingException(e);
}
}
}

public static DynamicData map(T entity) {
DynamicData data = null;
if (entity != null) {
data = new DynamicData();
try {
from(data, entity);
} catch (Exception e) {
throw new MappingException(e);
}
}
return data;
}

private static void to(DynamicData data, Object inst) throws Exception {
PropertyDescriptor[] desc = ReflectUtils.getBeanProperties(inst.getClass());
Map propMap = new HashMap();
for (PropertyDescriptor d : desc) {
propMap.put(d.getName(), d);
}
LinkedHashMap map = data.getMap();
for (Entry entry : map.entrySet()) {
String key = entry.getKey();
try {
DataProperty prop = entry.getValue();
PropertyDescriptor d = propMap.get(key);
if (d != null) {
Object value = prop != null ? prop.getValue() : null;
//log.debug("PROP: {}, VALUE: {}", key, value);
if (value instanceof DynamicData) {
Method mth = d.getReadMethod();
if (mth != null) {
Object bean = mth.invoke(inst, new Object[0]);
if (bean != null) {
to((DynamicData) value, bean);
}
}
} else {
Method mth = d.getWriteMethod();
if (mth != null) {
//Class clas = mth.getParameterTypes()[0];
//log.debug("Parameter class: {}", clas);
mth.invoke(inst, value);
}
}
}
} catch (Exception ex) {
throw new MappingException("Error for Parameter " + key, ex);
}
}
}

private static void from(DynamicData data, Object inst) throws Exception {
PropertyDescriptor[] desc = ReflectUtils.getBeanProperties(inst.getClass());
for (PropertyDescriptor des : desc) {
Method mth = des.getReadMethod();
if (mth != null) {
DynamicDataProperty an = mth.getAnnotation(DynamicDataProperty.class);
if (an != null) {
String key = des.getName();
Object result = mth.invoke(inst, new Object[0]);
DataProperty prop = null;
if (result != null) {
if (an.embeded()) {
DynamicData embeded = new DynamicData();
from(embeded, result);
result = embeded;
}
prop = new DataProperty(result);
}
data.getMap().put(key, prop);
}
}
}
}

}


Serializator ma dwie metody statyczne from i to służące do serializacji i deserializacji przekazywanych komponentów. Serializacja polega na zapisaniu property z obiektu domenowego w postaci mapy property, gdzie kluczem jest nazwa a wartością obiekt komponentowy DataProperty zawierający wartość odczytaną z obiektu domenowego. W przypadku property typu embeded tworzony nowy obiekt DynamicData i zapisywany jako wartość w odpowiednim DataProperty.

No dobrze mamy część serwerową no to czas na część kliencką i oto i ona:

public class DynamicDataSource extends TableDataSource {

private static final Converter CONVERTER = new Converter();

public DynamicDataSource(DataServiceAsync service, SortSpecifier[] sort) {
super(service, sort);
}

public DynamicDataSource(DataServiceAsync service) {
super(service);
}

@Override
public IConverter getRecordConverter() {
return CONVERTER;
}

static class Converter implements IConverter {

@Override
public void convertData(Record rec, DynamicData element) {
this.convertData(rec, null, element);
}

@Override
public DynamicData convertRecord(Record rec) {
DynamicData data = new DynamicData();
String[] attrs = rec.getAttributes();
for (String attr : attrs) {
if (Log.isDebugEnabled()) {
Log.debug("Processing attribute: " + attr);
}
LinkedHashMap map = getMap(attr, data);
Object value = rec.getAttributeAsObject(attr);
int idx = attr.lastIndexOf('.');
if (idx > -1) {
attr = attr.substring(idx + 1);
}
DataProperty dp = null;
if (value != null) {
if (value instanceof JavaScriptObject) {
value = JSOHelper.convertToJava((JavaScriptObject) value);
}
if (Log.isDebugEnabled()) {
Log.debug("VALUE calss: " + value.getClass() + " - " + value);
}
dp = new DataProperty(value);
}
map.put(attr, dp);
}
return data;
}

private LinkedHashMap getMap(String attr, DynamicData data) {
LinkedHashMap map = data.getMap();
int idx = attr.indexOf('.');
if (idx > -1) {
String at = attr.substring(0, idx);
DataProperty dp = map.get(at);
if (dp == null) {
DynamicData md = new DynamicData();
dp = new DataProperty(md);
map.put(at, dp);
}
return getMap(attr.substring(idx + 1), (DynamicData) dp.getValue());
}
return map;
}

private void convertData(Record rec, String prop, DynamicData element) {
LinkedHashMap map = element.getMap();
for (Entry entry : map.entrySet()) {
Object value = null;
String property = prop != null ? prop + "." + entry.getKey() : entry.getKey();
DataProperty dp = entry.getValue();
if (dp != null) {
value = dp.getValue();
}
if (value instanceof DynamicData) {
convertData(rec, property, (DynamicData) value);
} else {
rec.setAttribute(property, value);
}
}
}

}
}


Część kliencka jest w postaci DataSource i mamy tutaj metodę convertData() która służy do konwersji obiektu generycznego DTO na atrybuty w rekordzie DataSource-a oraz metodę convertRecord(), która konwertuje dane z rekordu na odpowiedni obiekt generyczny DTO.

0 komentarze:

Prześlij komentarz

GlossyBlue Blogger by Black Quanta. Theme & Icons by N.Design Studio
Entries RSS Comments RSS