pt>

poniedziałek, 20 lutego 2012

Dynamiczne tworzenie obiektów w GWT + MVP4G

    GWT jest dość udanym produktem, jednak ma swoje ograniczenia - głównie w dziedzinie refleksji. Najbardziej przydatną cechą jest tworzenie nowych obiektów oraz wnioskowanie typów obiektów. O ile w stosunku do drugiego problemu sprawę, w większości przypadków, rozwiązuje java generics to rozwiązanie pierwszego problemu za pomocą GWT.create(<class literal>) jest mocno ogranczone. Szczególnie bolesny staje się ten problem w aplikacjach gdzie część obiektów jest tworzonych z łańcuchów znaków - np pochodzących z formularzy które są generowane na podstawie klasy obiektów. Prosty scenariusz przewiduje iż chcemy wyświetlić w tabeli obiekty z bazy danych - tu z pomocą przychodzi MyBatis umożliwiający  mapowanie rekordów na obiekty, ale pojawia się wymaganie wprowadzania nowych rekordów przez generowaną formatkę (nikomu nie będzie się chciało pisać 50 formularzy dla każdej klasy osobno - nie uwierzę!). Formatka zawiera etykietowane pola/comboboxy/slidery - jednak zwracać potrafi tylko zestawy typów prymitywnych. Dla ułatwienia formatka zwraca jedynie tablicę stringów, by nowy "rekord" umieścić w tabelce (CellTable jest parametryzowana typem i wymaga obiektów) musimy uzyskać obiekt. Wiemy jakiej klasy chcemy mieć owy obiekt - pierwszą myślą jest stworzenie nowego obiektu za pomocą konstruktora bezparametrowego a następnie ustalenie pól tego  obiektu. Naturalnym rozwiązaniem wydaje się zastosowanie GWT.create( class clazz) - tu czeka nas niemiła niespodzianka - metoda ta przyjuje jedynie literały klas. Nie zadziała w momencie gdy parametrem jest obiekt Class<T>! Krótkie poszukiwania naprowadziły mnie na trop dwóch bibliotek GWT Reflection oraz GWT ENT. Pierwszy projekt niestety już od dłuższego czasu nie jest rozwijany, drugi wydawał się rokować większe nadzieje - niestety w połączeniu z GWT 2.4 owa biblioteka powoduje błędy, rozwiązaniem okazało się wrócenie do GWT wersji 2.2 - kompletnie nieakceptowalne rozwiązanie! Kolejne poszukiwania doprowadziły mnie do wniosku że trzeba samodzielnie wygenerować klasę fabrykującą obiekty, na rozwiązanie naprowadził mnie temat na stackOverflow oraz jednego z jugowiczów (dzięki Michał!).
    Plan działania jest stosunkowo prosty. Należy oznaczyć klasy, których obiekty mają być tworzone za pomocą interfejsu, który dosłownie nic nie robi, służy jedynie jako znacznik. Następnie należy stworzyć interfejs z jedyną metodą - instantiate(Class<T> clazz) i napisać klasę-generator, która będzie generowała klasę implementującą ten interfejs. Na koniec należy zmodyfikować deskryptor projektu *.gwt.xml. Tyle teorii - pora na częśćpraktyczną.



Potrzebne biblioteki/narzędzia :
- Eclipse/GWT Plugin / MVP4G - linki w moim pierwszym poście.

    Pierwszym krokiem jest stworzenie projektu GWT wykorzystującego MVP4G - proces ten dokładnie opisałem w pierwszym poście mojego bloga. Gdy posiadamy już podstawową strukturę projektu należy stworzyć widok - będzie to combobox z listą typów obiektów do stworzenia, przycisk zatwierdzający oraz panel do, którego będzie dodawana automatycznie stworzona tabelka.

package tutek2.client.view;


import tutek2.client.presenter.MainPresenter;
import com.google.gwt.core.client.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.FlowPanel;


public class MainView extends Composite implements MainPresenter.IView
{
 private static MainViewUiBinder uiBinder = GWT.create(MainViewUiBinder.class);
 
 @UiField 
 public Button btn_choose;
 
 @UiField 
 public ListBox lst_objects;
 
 @UiField 
 public FlowPanel panel_main;
 
 @UiField 
 public FlowPanel panel_tab;

 interface MainViewUiBinder extends UiBinder <Widget, MainView>
 {
 }

 public MainView()
 {
  initWidget(uiBinder.createAndBindUi(this));
 }
}

Następnie XML towarzyszący widokowi :
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
 xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:  ="urn:import:com.sun.xml.internal.bind.v2.schemagen.xmlschema">
 <ui:style>
  
 </ui:style>
 <g:HTMLPanel height="800px" width="100%">
  <g:VerticalPanel>
   <g:FlowPanel ui:field="panel_main">
    <g:ListBox width="100" ui:field="lst_objects"/>
    <g:Button ui:field="btn_choose">Wybierz</g:Button>
    <g:FlowPanel ui:field="panel_tab"/>
   </g:FlowPanel>
  </g:VerticalPanel>
 </g:HTMLPanel>
</ui:UiBinder> 

    Jest to zwykły widok i nie ma nic po za implementacją pustego interfejsu MainPresenter.IView zdefiniowanego w klasie prezentera. Zabieg ten służy pokazaniu w jaki sposób organizować interakcję między prezenterem a widokiem. Następnie deklarujemy interfejs eventBus, jedynie ze zdarzeniem startowym, które jest obowiązkowe we fframeworku MVP4G :
package tutek2.client;

import tutek2.client.presenter.MainPresenter;
import com.mvp4g.client.annotation.Event;
import com.mvp4g.client.annotation.Events;
import com.mvp4g.client.annotation.Start;
import com.mvp4g.client.event.EventBus;

/**
 * @author Kyniek
 *
 */
@Events(startPresenter = MainPresenter.class)
public interface MainEventBus extends EventBus
{ 
 @Start
 @Event(handlers = { MainPresenter.class})
 public void start();
}

    Narazie kod jest prosty - podobnie jak framework MVP4G ;) Teraz jeszcze prościej - deklarujemy interfejs będący owym znacznikiem klas dla których należy stworzyć fabrykę. Interfejs nazywa się znacząco MarkerBean.
package tutek2.shared.beans;

public interface MarkerBean
{
}

Kolejnym etapem jest stworzenie klasy bazowej dla naszych beanów" :
package tutek2.shared.beans;

import java.io.Serializable;
import java.util.HashMap;


public class BasicBean implements Serializable, MarkerBean
{ 
 private static final long serialVersionUID = -4888604278147507086L;

 //map of fields
 //name of property always is string type
 HashMap<String, Object> valMap;
 
 String[] cols;
 
 public BasicBean()
 {
  this.valMap = new HashMap<String, Object>();
 }
  
 public String[] getColls()
 {
  return cols;
 }
  
 public String getValue(String  col)
 {
  if(valMap.get(col) != null)
  {
   return valMap.get(col).toString();
  }
  return "";
 }
  
 public void setFields(String[] fields)
 {
  for(int i = 0; i < fields.length; i++)
  { 
   valMap.put(cols[i], fields[i]);
  }
 } 
}

    Pierwszą rzeczą, którą zrobiłem było oznaczenie klasy za pomocą wcześniej zdefiniowanego interfejsu MarkerBean (linia 7). Następnie zdefiniowałem pola obiektów - mapę (linia 13) i tablicę (linia 15). Tablica stanowi opis nagłówka tabelki(CellTable) oraz, równocześnie klucze dla wartości zawartych w mapie. W ten sposób uzyskałem elastyczną strukturę opisującą rekordy w tabelce. Kolejnym elementem jest bezparametrowy konstruktor(linie 17-20) - konieczny do automatycznego tworzenia obiektów. Metoda getColls() (linia 22) konieczna dla automatycznego tworzenia tabelki, bowiem tabelka musi wiedzieć jak nazwać kolumny w nagłówku. Przedostatnim wyróżnionym elementem jest metoda getValue(String col) (linia 27)- jest ona wymagana przez dataProvidera w CellTable - więcej o tym dalej, podczas omawiania klasy dynamicznej tabelki. Ostatnim ważnym fragmentem jest metoda setFields(String[] fields) - konieczna dla zunifikowanego mechanizmu ustawiania wartości dla każdego obiektu. Ważna jest kolejność pól w tablicy będącej parametrem - nie jest to jednak problemem od kiedy formatka do wstawiania danych jest generowana na podstawie klasy obiektu. Zanim stworzymy klasę tabelki potrzebna jest jeszcze jedna rzecz - mianowicie interfejs, który będzie implementowany przez generator, zawiera tylko jedną metodę do tworzenia obiektów na podstawie obiektu klasy - Class clazz. Parę dostępnych na sieci tutoriali pokazuje generatory tworzące implementacje interfejsu na podstawie nazwy klasy w postaci łańcucha znaków. Osobiście uważam że przekazywanie tak delikatnej i ważnej informacji jak klasa, powinno być realizowane po przez obiekt, nie po przez łańcuch znaków - stąd ta różnica :
package tutek2.shared.beans;

public interface Reflection
{
 public  T instantiate( Class clazz );
}

    Teraz czas na stworzenie klasy dynamicznej tabelki wyświetlającej obiekty dziedziczące po BasicBean :
package tutek2.client.widget;
import tutek2.shared.beans.BasicBean;
import tutek2.shared.beans.Reflection;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.TextColumn;
import com.google.gwt.view.client.ListDataProvider;



public class CustomTable<T extends BasicBean> extends CellTable<T>
{
 ListDataProvider<T> f_pro;

 public CustomTable()
 {
  super();
 }  
 
 public void init(String[] p_colls)
 {  
  f_pro =  new ListDataProvider<T>();
  
  for(String tmCol : p_colls)
  {
   final String val = tmCol;
   
   TextColumn<T> col = new TextColumn<T>() 
   { 
    @Override
    public String getValue(T object)
    {     
     return ((BasicBean)object).getValue(val);
    }    
   };
   this.addColumn(col, val);
  }  
  f_pro.addDataDisplay(this);    
 }  
 
 @SuppressWarnings("unchecked")
 public <U extends BasicBean>void addRow(String row[], Class<U> genericTypeClass)
 {    
  U ob =  ((Reflection) GWT.create( Reflection.class )).instantiate( genericTypeClass );   
  ob.setFields(row);
  f_pro.getList().add((T)ob);  
 } 
}

    Klasa tabelki jest sparametryzowana (podobnie jak CellTable po której dziedziczy), jednak możliwe są obiekty dziedziczące po BasicBean (linia 12). Następnie definiujemy bezparametrowy konstruktor (linia 16), w zasadzie nie jest on konieczny - to bardziej z przyzwyczajenia :E. Bardzo ważna jest metoda init(String[] p_colls) (linia 21), w której na podstawie podanych, jako parametr, nazw kolumn tworzymy kolumny textowe. Kolumnami są anonimowe klasy wewnętrzne, muszą one definiować metodę getValue - klasa BasicBean również definiuje tą metodę dla której parametrem jest nazwa kolumny. Jednak najważniejsza część przed nami - metoda addRow(String row[], Class<U> genericTypeClass) (linia 43) pozwala dodawać do tabelki nowe obiekty. Jednak nie jako obiekty explicite a tablice - pochodzące z naszej mitycznej, wygenerowanej formatki do wprowadzania danych. W tej metodzie za pomocą drobnego oszustwa tworzymy obiekt klasy implementującej (nie mamy jej, wygenerujemy ją) interfejs Reflection i za pomocą stworzonego obiektu fabryki tworzymy obiekt. Zanim przyjrzymy się klasie generującej fabrykę implementującą Reflection spójrzmy jeszcze na klasę prezentera, który jest nieodłąccznie związany z widokiem :
package tutek2.client.presenter;

import java.util.HashMap;
import tutek2.client.MainEventBus;
import tutek2.client.view.MainView;
import tutek2.client.widget.CustomTable;
import tutek2.shared.beans.AdressBean;
import tutek2.shared.beans.BasicBean;
import tutek2.shared.beans.PersonBean;
import tutek2.shared.beans.Reflection;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.FlowPanel;
import com.mvp4g.client.annotation.Presenter;
import com.mvp4g.client.presenter.BasePresenter;

/**
 * @author Kyniek
 *
 */
@Presenter(view = MainView.class)
public class MainPresenter extends BasePresenter<MainView, MainEventBus>
{
 
 private HashMap<String, Class<? extends BasicBean>> typeMap;
 
 private String[][] rows;
 
 public MainPresenter()
 {
  GWT.log("constructor : MainPresenter");
  typeMap = new HashMap<String, Class<? extends BasicBean>>();
  typeMap.put("adresses", AdressBean.class);
  typeMap.put("persons", PersonBean.class);  
  
  rows = new String[][]{ 
    new String[]{"Wieśwawa", "Wiejska", "3"},
    new String[]{"Wąchock", "Śmiechowa", "7"},
    new String[]{"Jacek", "Rostek", "Duszenie zwierząc, masturbacja i golf"},
    new String[]{"Donek", "Obibok", "Streszczanie Marcela Prousta"}};        
 } 
 
 public interface IView{ }

 @Override
 public void bind()
 {
  GWT.log("bind : MainPresenter");
  
  for(String name : typeMap.keySet())
  {
   view.lst_objects.addItem(name);
  }  
  view.lst_objects.setSelectedIndex(0);
  
  view.btn_choose.addClickHandler(new ClickHandler() 
  {   
   @Override
   public void onClick(ClickEvent event)
   {
    fillTable(view.panel_tab, typeMap.get(view.lst_objects.getValue(view.lst_objects.getSelectedIndex())), rows, view.lst_objects.getSelectedIndex());
    
   }
  });
 } 
 
 public <U extends BasicBean> void fillTable(FlowPanel panel, Class<U> clazz, String[][] vals, int tabPart)
 {
  //GWT.log(clazz.toString());
  CustomTable<? extends BasicBean> tb = new CustomTable<BasicBean>();
  U ob =  ((Reflection) GWT.create( Reflection.class )).instantiate( clazz );
  tb.init(ob.getColls());
  
  for(int i = 0; i < 2; i++)
  {
   String[] row = vals[i + 2 * tabPart];
   tb.addRow(row, clazz);
  }
  panel.clear();
  panel.add(tb);
 }
  
 public void onStart()
 {
  GWT.log("Start!");
 } 
}

    Klasa prezentera przedstawia się raczej standardowo. Dość nieeleganckim rozwiązaniem było umieszczenia w nim danych - modelu (linie : 26, 28 i 37), symulują one generowaną formatkę potrafiącą zwracać jedynie tablice stringów.  W metodzie bind (l. 47), wywoływanej przez MVP4G, następuje konfiguracja listy kluczami mapy obiektów (typeMap, linia 26) oraz przypisanie obsługi zdarzeń. Kluczowym fragmentem w prezenterze jest metoda fillTable (l. 68) , tworzymy pustą tabelkę, jednak do jej skonfigurowania (metodą init) potrzebny jest nowy obiekt klasy której obiekty będą za jej pomocą prezentowane. W tym celu korzystamy z wygenerowanej fabryki - podobnie jak to miało miejsce wcześniej w klasie CustomTable.
    Pora przyjrzeć się w jaki sposób jest generowana klasa fabryki obiektów, dzięki której możliwe jest tworzenie we własnym zakresie namiastki refleksji w GWT :
package gen;



import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import tutek2.shared.beans.MarkerBean;
import tutek2.shared.beans.Reflection;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

public class ReflectionGenerator extends Generator
{    
    @Override
    public String generate( TreeLogger logger, GeneratorContext context, String typeName ) throws UnableToCompleteException
    {
        TypeOracle oracle = context.getTypeOracle( );
        JClassType instantiableType = oracle.findType( MarkerBean.class.getName( ) );//finds our marker interface

        List<JClassType> clazzes = new ArrayList<JClassType>( );
        

        for ( JClassType classType : oracle.getTypes( ) )
        {
            if ( !classType.equals( instantiableType ) && classType.isAssignableTo( instantiableType ) )//filters only classes, leaving interface-marker
                clazzes.add( classType );
        }

        final String genPackageName = "tutek2.shared.beans";//our "bean" package
        final String genClassName = "ReflectionImpl";//our Reflection implementation

      //creating out class generator
        ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory( genPackageName, genClassName );
        //setting implemented interface
        composer.addImplementedInterface( Reflection.class.getCanonicalName( ) );
        //adding imports
        composer.addImport( genPackageName + ".*" );

        PrintWriter printWriter = context.tryCreate( logger, genPackageName, genClassName );

        if ( printWriter != null )
        {
            SourceWriter sourceWriter = composer.createSourceWriter( context, printWriter );
            //default constructor
            sourceWriter.println(genClassName + "( ) {" );
            sourceWriter.println( "}" );

            printFactoryMethod( clazzes, sourceWriter );

            sourceWriter.commit( logger );
        }
        return composer.getCreatedClassName( );
    }

    private void printFactoryMethod( List<JClassType> clazzes, SourceWriter sourceWriter )
    {
        sourceWriter.println( );
        //implementing method from "Reflection" interface
        sourceWriter.println( "public <T, V extends T> T instantiate( Class<V> clazz ) {" );

        for ( JClassType classType : clazzes )
        {
            if ( classType.isAbstract( ) )
                continue;

            sourceWriter.println( );
            sourceWriter.indent( );
            sourceWriter.println( "if (clazz.getName().endsWith(\"." + classType.getName( ) + "\")) {" );
            sourceWriter.indent( );
            sourceWriter.println( "return (T) new " + classType.getQualifiedSourceName( ) + "( );" );
            sourceWriter.outdent( );
            sourceWriter.println( "}" );
            sourceWriter.outdent( );
            sourceWriter.println( );
        }
        sourceWriter.indent();
        sourceWriter.println("return (T) null;");
        sourceWriter.outdent();
        sourceWriter.println();
        sourceWriter.println("}");
        sourceWriter.outdent( );
        sourceWriter.println( );
    }
}

    Dziedziczymy po istniejącej klasie abstrakcyjnej (com.google.gwt.core.ext.Generator), nadpisując metodę generate(l. 22). Wewnątrz na początku, są tworzone dwa ważne obiekty, oracle(l. 24) - służy do pozyskania globalnej informacji o kodzie, w tym przypadku do pobrania wszystkich typów, natomiast instantiableType reprezentuje pojedyńczy typ, dokładniej interfejs znakujący klasy - MarkerBean. Warto zapoznać się z opisem powyższych klas, pomocne to będzie podczas pisania innych generatorów. Następnie ze wszystkich typów należy wybrać tylko klasy oznaczone interfejsem (l. 30). Obiekt composer(l. 40) posłuży nam do fizycznego wygenerowania klasy. Kod tylko na pozór wygląda na bardzo skomplikowany - w rzeczywistości klasę tworzy się w dość prymitywne sklejanie łańcuchów znakowych. Przeto dobrym sposobem jest pierw napisanie ręcznie klasy, którą chcemy wygenerować, a dopiero wtedy pisanie generatora. printFactoryMethod tak naprawdę "odwala całą brudną robotę", na podstawie wczesniej znalezionych klas, oznaczonych interfejsem, tworzymy zestaw konstruktorów dla każdej z klas. Na koniec by wszystko zadziałało potrzebny jest wpis w deskryptorze projektu *.gwt.xml :
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='tutek_mvp4g_2'>
  <!-- Inherit the core Web Toolkit stuff.                        -->
  <inherits name='com.google.gwt.user.User'/>

  <inherits name='com.google.gwt.user.theme.clean.Clean'/>

  <!-- Other module inherits                                      -->
  <inherits name='com.mvp4g.Mvp4gModule' />  

  <!-- Specify the app entry point class.                         -->
  <entry-point class='com.mvp4g.client.Mvp4gEntryPoint'/> 

  <!-- Specify the paths for translatable code                    -->
  <source path='client'/>
  <source path='shared'/>  
  
  <generate-with class="gen.ReflectionGenerator">
      <when-type-assignable class="tutek2.shared.beans.Reflection" />      
  </generate-with>
</module>

    Ważne są trzy ostatnie oznaczone linie : najpierw określamy klasę która ma zostać używa w roli generatora - gen.ReflectionGenerator, następnie należy wskazać w stosunku do jakich typów ma zostać użyty - w tym przypadku tylko do wygenerowania implementacji na podstawie interfejsu tutek2.shared.beans.Reflection. Zatem gotowe!


    Pokazany sposób nie jest ani najprostszy, ani najbardziej elegancki ale działa i w przypadku braku stabilnych bibliotek jest jedyną alternatywą. Niejako uzupełnieniem dla tej techniki jest AutoBean - framework będący częścią GWT więc o przykrych niespodziankach związanych z wersjami (patrz początek i GWT-ENT) nie ma mowy. Na koniec daję link do gotowego projektu w Eclipse.

Brak komentarzy:

Prześlij komentarz