SmartGWT: zamknięcie okna po operacji saveData()

, niedziela, 31 lipca 2011 0 komentarze

Przedstawiam rozwiązanie problemu dotyczącego zamknięcia okna po poprawnym wykonaniu operacji save. Problem polega na tym, że po wykonaniu kodu jak poniżej dostaniemy błąd NULL-owej referencji do okna.

public class DynamicFormWindow extends Window {
protected DynamicForm form;
private ActionPanel actionPanel = new ActionPanel();

public DynamicFormWindow(DynamicForm form, String title) {
this.form = form;

setShowShadow(true);
setShadowSoftness(4);
setShadowOffset(3);

this.setTitle(title);
this.setLayoutMargin(4);
this.setShowMinimizeButton(false);
this.setCanDragResize(true);
this.setAutoSize(true);
this.setIsModal(true);
this.setShowModalMask(false);
this.centerInPage();

this.addCloseClickHandler(new CloseClickHandler() {
@Override
public void onCloseClick(CloseClientEvent event) {
destroy();
}
});

VPanel panel = new VPanel();
panel.setPadding(2);
panel.addMember(form);
panel.addMember(this.actionPanel);

this.addItem(panel);

IButton saveButton = new IButton("Zapisz");
saveButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
DynamicFormWindow.this.saveData();
}
});
addActionButton(saveButton);
}

protected void saveData() {
this.form.saveData();
this.destroy();
}

protected void addActionButton(IButton button) {
this.actionPanel.addMember(button);
}

}


Problem polega na tym, że po wykonaniu saveData() automatycznie wykonywane jest destroy(), gdzie saveData() wykonuje akcję asynchronicznie używając GWT-RPC po stronie serwerowej aplikacji.

Rozwiązaniem problemu jest wykorzystanie Callbacka na saveData() oraz funkcjonalności Scheduler-a. W tym celu zmieniamy ciało metody saveData() w następujący sposób:


protected void saveData() {
if (this.form.validate()) {
this.form.saveData(new DSCallback() {
@Override
public void execute(DSResponse response, Object rawData, DSRequest request) {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
DynamicFormWindow.this.destroy();
}
});
}
});
}
}


Takie podejście daje nam to, że kod zamykający okno wykonywany jest po tym jak zakończy się operacja saveData() oraz bieżąca pętla obsługi zdarzeń GWT. Oznacza to, że kod zamykający okno wykona się po całkowitym zakończeniu obsługi związanej z akcją saveData().

Dodatkowe metody Schedulera scheduleEntry/scheduleFinally wykonywane są wewnątrz pętli obsługi zdarzeń zaraz przed/po tym jak wykonany zostanie kod związany z obsługą zdarzeń GWT. W przypadku wykonywania manipulacji DOM wewnątrz tej samej pętli obsługi zdarzeń, można przeciwdziałać dodatkowemu odmalowaniu przez browser.

Web Project Template, czyli Eclipse + Maven+ Spring + GWT + Hibernate

, , , , 0 komentarze

W tym artykule chciałbym podzielić się swoim doświadczeniem w ramach konfiguracji projektu typu Web wykorzystującego Spring, SmartGWT, Hiberante. Całość konfiguracji w zamierzeniu ma być budowana przez Maven-a. Zakładam, że do developmentu używane będzie środowisko Eclipse Helios SR 2. Dlatego zacznijmy od konfiguracji środowiska projektowego.

Zaczynamy od ściągnięcia Eclipsa ze strony Eclipse Helios SR2 Packages (v 3.6.2). Teraz, żeby nam się dobrze pracowało instalujemy następujące pluginy:
- Maven Integration for Eclipse Update Site
- Google Update Site for Eclipse 3.6
- GWT Designer Update Site
- SpringSource Update Site for Eclipse 3.6
- SpringSource Update Site for Eclipse 3.6 (Dependencies)
- JBoss Tools Development Milestone Update Site
Po instalacji Eclipse oraz wymienionych pluginów jesteśmy przygotowani do pracy, więc możemy zacząć. Moja propozycja podziału na moduły aplikacji typu Web jest następująca:
- moduł dziedzinowy: zawierający zarówno model dziedzinowy jak i operacje związane z modelem, czyli inaczej mówiąc model JPA oraz związane z nimi DAO (Data Access Objects)
- moduł usług: zawierający bean-y Springowe udostępniające usługi biznesowe w oparciu o model dziedzinowy
- moduł warstwy web: moduł zawierający całość logiki klienckiej, udostępniający warstwę UI dla użytkownika końcowego.

Moduł warstwy web komunikuje się z modułem serwisów w celu wykonywania akcji po stronie serwera. Nie ma on dostępu do modułu domenowego, gdyż moduł serwisów izoluje dostęp do warstwy domenowej.

Zajmijmy się teraz konfiguracją modułów. W tym celu utworzyć należy strukturę plików tak jak zostało to przedstawione na rysunku obok.

Najważniejsze w tym momencie są pliki pom.xml, czyli pliki konfiguracyjne Mavena. Teraz zajmę się ustawieniami w tych plikach. Dobrze, główny plik konfiguracyjny (root) znajdujący się na poziomie katalogów m2-domain, m2-service, m2-web wygląda następująco:

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0
pl.masl.m2
m2-root
1.0.0-SNAPSHOT
pom
M-2 :: ${project.artifactId}


UTF-8
1.6
1.6
4.8.2
3.0.5.RELEASE
3.0.5.RELEASE
3.5.6-Final
1.0.0.Final
2.0.8
5.1.10



m2-domain
m2-service
m2-web




junit
junit
${junit.verion}
test


org.slf4j
jcl-over-slf4j
1.5.8


org.slf4j
slf4j-log4j12
1.5.8





maven-annotation-plugin

http://maven-annotation-plugin.googlecode.com/svn/trunk/mavenrepo






Central Repo
http://repo1.maven.org/maven2


com.springsource.repository.maven.release
http://maven.springframework.org/release/

false



com.springsource.repository.maven.snapshot
http://maven.springframework.org/snapshot/

true



com.springsource.repository.maven.milestone
http://maven.springframework.org/milestone/

false



SmartGWT Repo
http://www.smartclient.com/maven2/


JavaNet
http://download.java.net/maven/2




Co widać w tym pliku to ustawienia dotyczące wersji bibliotek używanych w ramach aplikacji, definicję modułów z których składa się aplikacja oraz definicja repozytoriów dla bibliotek.

Następnie konfigurujemy plik pom.xml dla modułu domenowego:
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0


m2-root
pl.masl.m2
1.0.0-SNAPSHOT


pl.masl.m2
m2-domain
1.0.0-SNAPSHOT
M-2 :: ${project.artifactId}



org.hibernate
hibernate-jpamodelgen
${hibernate-jpa.version}


org.hibernate
hibernate-entitymanager
${hibernate.version}


org.hibernate
hibernate-c3p0
${hibernate.version}


org.hibernate
hibernate-ehcache
${hibernate.version}


org.springframework
spring-test
${spring.version}


org.springframework.data
spring-data-jpa
1.0.0.M2


org.springframework
spring-orm
${spring.version}


mysql
mysql-connector-java
${mysql.connector.version}






org.apache.maven.plugins
maven-compiler-plugin
2.0.2

${maven.compiler.source}
${maven.compiler.target}
${project.build.sourceEncoding}
-proc:none




org.bsc.maven
maven-processor-plugin
1.3.1


process

process

generate-sources

target/metamodel


org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor








org.codehaus.mojo
build-helper-maven-plugin
1.3


add-source
generate-sources

add-source



target/metamodel










Co mamy w tym pliku to zależności od hibernate-a i Springa, definicja pluginu do kompilacji, definicja pluginów do generacji modelu JPA 2.0.

Teraz konfiguracja pliku pom.xml dla modułu usług:
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0

m2-root
pl.masl.m2
1.0.0-SNAPSHOT

pl.masl.m2
m2-service
1.0.0-SNAPSHOT
M-2 :: ${project.artifactId}



pl.masl.m2
m2-domain
1.0.0-SNAPSHOT
jar
compile


aspectj
aspectjrt
1.5.4


aspectj
aspectjweaver
1.5.4


net.sf.dozer
dozer
5.3.2


org.springframework
spring-tx
${spring.version}


org.springframework.security
spring-security-config
${springframework.security.version}






org.apache.maven.plugins
maven-compiler-plugin
2.3.2

${maven.compiler.source}
${maven.compiler.target}
${project.build.sourceEncoding}



org.apache.maven.plugins
maven-source-plugin
2.1.2


attach-sources
verify

jar-no-fork








W tym pliku widać definicję zależności od modułu domenowego, bibliotek Springa oraz pluginów do kompilacji.

No i na koniec definicja pom.xml-a dla modułu webowego:

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">


m2-root
pl.masl.m2
1.0.0-SNAPSHOT


4.0.0
pl.masl.m2
m2-web
war
1.0.0-SNAPSHOT
M-2 :: ${project.artifactId}



2.3.0
2.4
${project.build.directory}/${project.build.finalName}




pl.masl.m2
m2-service
1.0.0-SNAPSHOT


com.google.gwt
gwt-user
${gwtVersion}
provided


com.google.gwt
gwt-servlet
${gwtVersion}
compile


javax.validation
validation-api
1.0.0.GA
test


javax.validation
validation-api
1.0.0.GA
sources
test


com.smartgwt
smartgwt
${smartgwt.version}
compile


com.smartgwt
smartgwt-skins
${smartgwt.version}
compile


com.google.code
gwt-log
3.1.3


net.sf.gwt-widget
gwt-sl
1.3-RC1
jar
compile


gwt-servlet
com.google.gwt


cglib-nodep
cglib




org.springframework
spring-web
${spring.version}


org.springframework
spring-webmvc
${spring.version}



org.springframework.security
spring-security-config
${springframework.security.version}


org.springframework.security
spring-security-web
${springframework.security.version}




${webappDirectory}/WEB-INF/classes
m2



org.codehaus.mojo
gwt-maven-plugin
2.3.0-1



compile
test
i18n
generateAsync





pl.masl.m2:m2-service

M2.html
${webappDirectory}
pl.masl.m2.web.gwt.client.Constants


pl.masl.m2.web.gwt.client.Messages




org.apache.maven.plugins
maven-war-plugin
2.1.1


compile

exploded




${webappDirectory}



org.apache.maven.plugins
maven-compiler-plugin
2.3.2

${maven.compiler.source}
${maven.compiler.target}
${project.build.sourceEncoding}






Tutaj mamy uzależnienie od bibliotek Springa, bibliotek GWT, bibliotek SmartGWT, ustawień dla kompilacji GWT oraz ustawień dla modułu typu WEB.

Teraz zajmiemy się wciągnięciem modułów do Eclipsa.

Obsługa DTO dla Spring i SmartGWT

, , 0 komentarze

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.

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