Serwisy REST Webhandler po raz trzeci

W 2018 opisałem 2 przykłady serwisów REST Webhandler ABL. Dostęp do nich jest nie poprzez warstwę transportową REST lecz WEB.

Temat powraca ze względu na Wasze zainteresowanie tą metodą. Intuicyjnie, niektórzy uważają, że jest to lepszy sposób na budowanie serwisów po przejściu na serwer aplikacji PASOE, ze względu na to że są to serwisy napisane w języku ABL. Niektórzy uważają jednakże, że ten typ serwisów jest trudniejszy do wygenerowania w porównaniu z typowymi serwisami REST Data Object Service. Pokażę za chwilę, że serwisy z użyciem Web Handera (w skrócie WH) są również bardzo proste.
W niniejszym blogu znajdziecie 2 metody tworzenia typowych serwisów REST: Data Object Service oraz z mapowaniem elementów URI. Pierwsza metoda jest bardzo łatwa, ale ma pewne ograniczenia, druga bardziej elastyczna, ale wymagająca więcej pracy.

Z serwisami WH jest podobnie. To co pokazałem 2 lata temu, to trudniejsza, elastyczna metoda związana z napisaniem własnego web handlera. Jednakże, można użyć szybkiej, prostej metody Data Object Service, co teraz chciałbym pokazać.

Tworzymy projekt typy WEB. Zaznaczamy “Create a Data Object Service….”.

Możemy poprzestać na domyślnej nazwie aplikacji webowej, lub nadać własną: tutaj wh. Aplikacja będzie wdrożona na serwerze oepas1.

Serwis ABL będzie miał nazwę whsrv.

Projekt jest podłączony do bazy danych sports2000. Naciskamy Finish.

Projekt został utworzony. Teraz zdefiniujemy klasę Business Entity o nazwie customer.

Wybieramy tabelę Customer z bazy sports2000 oraz nazwę zasobu w URI: /customer.

Prawym klawiszem myszy klikamy na wygenerowany serwis i Edit. Zaznaczamy klasę customer.cls, która ma być źródłem dla serwisu. Naciskamy Finish.

Warto zaznaczyć, że dla serwisów typy WEB obok pliku .json jest wygenerowany plik .gen. Możemy to zauważyć w widoku konsoli.

Teraz pora na restart pasoe i sprawdzenie naszego serwisu. W przeglądarce (tutaj Firefox) wpisujemy localhost:[port]/wh/web/pdo/whsrv/customer

W adresie możemy skorzystać z filtra; chcemy np. tylko rekordy z Finlandii: localhost:[port]/wh/web/pdo/whsrv/customer?filter=Country=”Finland”

Proste, prawda?

Jakie są rekomendacje Progress Software dotyczące wyboru warstwy transportowej i metody budowania serwisów wg warstwy REST czy WEB.

REST – oparte na Javie, są rekomendowane przy wystawianiu istniejącej, restowej logiki biznesowej, gdy nie chcemy pisać kodu od początku.

WEB – oparte na języku ABL, o wiele bardziej skalowalne. Są rekomendowane jeśli chcemy pisać serwisy od zera.

PASOE i WebSpeed

Myślę, że wielu spośród polskich klientów Progressa pamięta czasy wersji 9 (koniec lat 90-tych), z którą wprowadzona została technologia tworzenia aplikacji webowych WebSpeed. Składał się on z dwóch produktów: WebSpeed Transaction Servera – odpowiedzialnego za uruchamianie i poprawność działania aplikacji, zapewniającego spójność transakcji i danych przy jednoczesnym korzystaniu z wielu baz. Drugim produktem był WebSpeed Workshop, złożone środowisko do tworzenia aplikacji.

Pamiętam, że technologia ta cieszyła się sporym zainteresowaniem naszych klientów, co zaowocowało kilkoma szkoleniami z tego zakresu. W wersji OpenEdge 10, WebSpeed został scalony z produktem OpenEdge AppServer. Po wprowadzeniu nowego serwera aplikacji PASOE i zaprzestaniu rozwijania klasycznego AppServera pojawiło się pytanie: co dalej z aplikacjami WebSpeed?

Zanim odpowiem na to pytanie chciałbym krótko przypomnieć w jaki sposób można było tworzyć aplikacje WebSpeed. Użytkownik miał do wyboru trzy możliwości.

Pierwszym był plik HTML, w którym można było osadzić elementy języka SpeedScript pomiędzy znacznikami <script language=”SpeedScript”> … </script>. SpeedScript, to był w zasadzie język 4GL z wyłączeniem operacji wejścia/wyjścia. Zamieszczam prosty przykład; operacje wyjścia kierowane są do predefiniowanego strumienia.

<HTML>
  <HEAD>
  </HEAD>
  <BODY>
    <H1> Customer List </H1>
    <SCRIPT LANGUAGE=”SpeedScript”>
      FOR EACH customer:
        DISPLAY {&WEBSTREAM} customer.
      END.
    </SCRIPT>
  </BODY>
</HTML>

Drugim sposobem było niejako odwrócenie ról; językiem nadrzędnym był SpeedScript a znaczniki HTML były osadzane w strumieniu {&OUT}, jak poniżej…

PROCEDURE ws-out:
{&OUT}
 „<HTML>”:U SKIP
 „<BODY>”:U SKIP
 .
  FOR EACH customer:
   {&DISPLAY} customer.
  END.
{&OUT}
 „</BODY>”:U SKIP
 „</HTML>”:U SKIP
 .
END PROCEDURE.

Wreszcie ostatnim sposobem, najbardziej rozbudowanym było mapowanie HTML. Tworzone były wówczas 2 pliki: HTML (zwykle forma z polami i przypisaną akcją) oraz plik .w.

Otóż, w nowym serwerze aplikacji można obsługiwać dwa pierwsze typy aplikacji. Jednakże, jeśli przypomnimy sobie 2 artykuły PASOE i serwisy WebHandler zobaczymy, że ta nowa technologia daje nam o wiele większe możliwości.

Sposób otwierania naszych aplikacji .w jest bardzo prosty.

Załóżmy, że mamy skonfigurowaną instancję serwera aplikacji, która połączona jest do bazy danych sports2000. Chcemy otworzyć plik w-custlist.w, będący prostą aplikacją WebSpeeda.

W katalogu roboczym naszej instancji przechodzimy do podkatalogu: %WRK%\instancja\webapps\ROOT\WEB-INF\openedge i umieszczamy tam plik .w. (oczywiście zamiast słowa ‘instancja’ umieszczamy nazwę naszego pasoe).

Następnie otwieramy plik %WRK%\instancja\conf\openedge.properties
i szukamy tam frazy: instancja.ROOT.WEB.defaultHandler.
Powinna mieć wartość: OpenEdge.Web.OpenEdge.Web.CompatibilityHandler.
Jeśli tak nie jest uruchamiamy komendę:
oeprop instancja.ROOT.WEB.defaultHandler=OpenEdge.Web.OpenEdge.Web.CompatibilityHandler
Wpis w pliku konfiguracyjnym będzie miał zatem postać:

[instancja.ROOT.WEB]
    defaultCookieDomain=
    wsRoot=/static/webspeed
    srvrDebug=1
    defaultHandler=OpenEdge.Web.CompatibilityHandler

Oczywiście możemy także wartości parametrów wpisywać ręcznie, ale łatwiej wtedy o pomyłkę. Zawsze najpierw warto zrobić kopię pliku konfiguracyjnego.

Restartujemy instancję serwera i wpisujemy w przeglądarce URL:

http://localhost:8810/web/w-custlist.w

Proszę zauważyć, że wykorzystywana jest czwarta warstwa komunikacji – WEB. Zaglądając do wspomnianych wcześniej 2 artykułów można spróbować zdefiniować klasy obsługujące różne wywołania dla odpowiednio podanych wartości URL.

Tworzenie obiektowego UI w OE Development Studio cz. IV

W czwartej części naszego cyklu, zajmiemy się obiecanym tematem edycji rekordów w bazie danych. Przypomnijmy: dane wyświetlane w obiektach graficznych są zasilane ze źródła BindingSource.
To źródło pobiera dane z tabeli np. Customer czy Order, ale działa niejako w jednym kierunku, tzn. nie nadaje się do zapisu danych. W naszych przykładach zapis będzie realizowany do tabel w bazie poprzez zdefiniowane statyczne bufory rekordów.

W przypadku aplikacji działającej np. na serwerze aplikacji, gdzie nie ma bezpośredniego dostępu do bazy, źródło BindingSource konfiguruje się w oparciu o schemat ProDataSet, z tabelami tymczasowymi, a edycja jest realizowana poprzez zapis do tych właśnie obiektów.

OK, zaczynamy.

W formie fCustomer dodajemy przycisk z etykietą Delete, nazwa np. Del.
Klikamy dwa razy w zdarzenie Click aby utworzyć metodę do obsługi tego zdarzenia (Del_Click). W metodzie tej wstawiamy poniższy kod:

DEFINE BUFFER bCust FOR Customer.
DEFINE BUFFER bOrder FOR Order.
DEFINE VARIABLE lDel AS LOGICAL NO-UNDO.
DEFINE VARIABLE iActCustNum AS INTEGER NO-UNDO.
		
MESSAGE "Do you want to delete this record?" SKIP 
            bsCust:InputValue["Name"]:ToString() SKIP 
	    bsCust:InputValue["City"]:ToString() SKIP
VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO UPDATE lDel.
IF NOT lDel THEN 
   RETURN.

Na razie mamy sukces tylko połowiczny – pojawia się komunikat gdzie możemy podjąć decyzję o kasowaniu rekordu.

Teraz dodamy kod kasujący rekord Customer i połączone z nim rekordy Order.
Dodajemy kod poniżej instrukcji RETURN:

IF NOT lDel THEN 
   RETURN.

iActCustNum = INTEGER(bsCust:InputValue["CustNum"]:ToString()).
DO TRANSACTION ON ERROR UNDO, RETURN:
   FOR FIRST bCust WHERE bCust.CustNum = iActCustNum:
      FOR EACH bOrder WHERE bOrder.CustNum = bCust.CustNum.
          DELETE bOrder.
      END.
      DELETE bCust.
   END. 
END. 

Żeby program zadziałał trzeba pamiętać, że w bazie są jeszcze trygery, zabezpieczające przed utratą danych. Trzeba albo te trygery zmodyfikować albo wyłączyć. Decyzja należy do Was.

Drugim problemem jest to, że po skasowaniu rekordu DataGrid nie odświeża automatycznie danych; musimy to zrobić sami.
Dodajemy w metodzie kod:

bsCust:MovePrevious().
iPos = bsCust:POSITION.
refreshData().
bsCust:POSITION = iPos.

Zmienną iPos deklarujemy jako integer. Po skasowaniu kursor ustawia się na poprzednim rekordzie.

Teraz dodajemy przyciski Edit i Create i generujemy metody dla zdarzenia Click.

Przeważnie te same funkcjonalności można stworzyć na różne sposoby. Można np. wykorzystać możliwości jakie daje DataGrid, ale wg Wiesława, ta metoda może generować wiele problemów i nie jest zbyt czytelna. Wygodniej jest stworzyć oddzielne okno dla edycji. Tworzymy więc nową formę fCustEdit.

Dodajemy 4 pola TextBox: CustNum, Name, City, Country, oraz etykiety dla tych pól.

Wstawiamy przyciski btnOK (etykieta OK), btnCancel (etykieta Cancel) i ustawiamy ich predefiniowaną funkcjonalność.

We właściwościach fCustEdit ustawiamy parametr CancelButton -> btnCancel.
We właściwościach btnOK ustawiamy DialogResult -> OK.

Wracamy do okna fCustomer, do metody dla kliknięcia przycisku Edit. Wstawiamy tu kod:

METHOD PRIVATE VOID Edit_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   DEFINE VARIABLE oCustEdit AS fCustEdit NO-UNDO.
   oCustEdit = NEW fCustEdit().
   WAIT-FOR oCustEdit:ShowDialog().
   DELETE OBJECT oCustEdit.

   RETURN.

Przycisk Edit już działa choć na razie nie widzimy danych.

Aby przekazać dane do edycji można skorzystać z różnych metod. My zdefiniujemy w fCustEdit właściwości dla każdego edytowanego pola. Np. pName dla pola Name; i tak dalej dla pozostałych pól.

    DEFINE PUBLIC PROPERTY pName AS CHARACTER NO-UNDO 
    GET.
    SET. 

Generujemy obsługę zdarzenia Shown dla fCustEdit i wpisujemu kod:

METHOD PRIVATE VOID fCustEdit_Shown( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   ASSIGN
      textBoxName:TEXT = pName
      textBoxCity:TEXT = pCity
      textBoxCountry:TEXT = pCountry
      textBoxCustNum:TEXT = string(pCustNum).
   RETURN.

END METHOD.

Nasze okno edycyjne będzie pokazywać dane ustawione w fCustomer. Ponieważ pole CustNum nie może być przez nas edytowane ustawiamy w jego właściwościach Enabled = False, a żeby wyłączyć ramkę wokół pola ustawiamy BorderStyle = None.

Aby zapisać dane po edycji dodajemy obsługę kliknięcia na przycisk OK.

METHOD PRIVATE VOID btnOK_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   ASSIGN 
      pName    = textBoxName:Text
      pCity    = textBoxCity:Text
      pCountry = textBoxCountry:TEXT.
   RETURN.

END METHOD.

Wracamy do fCustomer i ustawiamy właściwości do wyświetlenia w oknie fCustEdit, czyli dodajemy kod w Edit_Click:

oCustEdit = NEW fCustEdit().
		   	
oCustEdit:pCustNum = int(bsCust:InputValue["CustNum"]:ToString()).
oCustEdit:pName = bsCust:InputValue["Name"]:ToString().
oCustEdit:pCity = bsCust:InputValue["City"]:ToString().
oCustEdit:pCountry = bsCust:InputValue["Country"]:ToString().
iActCustNum = INT(bsCust:InputValue["CustNum"]:ToString()).
		
WAIT-FOR oCustEdit:ShowDialog().

Okno fCustEdit wygląda teraz tak:

Następnym krokiem jest zapis wprowadzonych danych w fCustomer na dole obsługi Edit_Click. Dodajemy transakcję:

WAIT-FOR oCustEdit:ShowDialog().

DO TRANSACTION ON ERROR UNDO, RETURN:
   FOR FIRST bCust WHERE bCust.CustNum = iActCustNum SHARE-LOCK :
      ASSIGN
         bCust.Name    = oCustEdit:pName
         bCust.City    = oCustEdit:pCity
         bCust.Country = oCustEdit:pCountry.
   END.
END.

bsCust:Refresh().

OK, edycja jest już obsłużona. Pozostało jeszcze dodanie obsługi dla utworzenia nowego rekordu.

W tym celu wykorzystamy okno fCustEdit i fragment kodu dla edycji. Nie musimy przekazywać wartości pól, ale tworzymy nowy rekord komendą CREATE. Kod dla przycisku Create wygląda tak:

DEFINE BUFFER bCust FOR Customer.
DEFINE VARIABLE oCustEdit AS fCustEdit NO-UNDO.
DEFINE VARIABLE rRowid AS ROWID NO-UNDO.
        
oCustEdit = NEW fCustEdit().
                   
WAIT-FOR oCustEdit:ShowDialog().		
        
DO TRANSACTION ON ERROR UNDO, RETURN:
   CREATE bCust.
   ASSIGN
      bCust.Name    = oCustEdit:pName
      bCust.City    = oCustEdit:pCity
      bCust.Country = oCustEdit:pCountry.
   rRowid = ROWID(bCust).
END.

refreshData().
bsCust:HANDLE:reposition-to-rowid(rRowid).
RETURN.


Zmienna ROWID jest potrzebna żeby po dodaniu ustawić się na nowym rekordzie.
Małą niedogodnością wykorzystania formy fCustEdit jest to, że numer rekordu wynosi 0, ale z tym powinniście sobie poradzić bez problemu np. przenosząc instrukcję CREATE przed wywołanie formy i przekazanie parametru CustNum.


W zasadzie program jest gotowy, ale Wiesław chciał na koniec uatrakcyjnić go nieco dla Was.
Po pierwsze dodajmy klawisze skrótów dla operacji na rekordach.

We właściwościach formy fCustomer ustawiamy KeyPreview = True.

Następnie dla gridu włączamy zdarzenie KeyDown.

W metodzie obsługującej to zdarzenie definiujemy:

METHOD PRIVATE VOID dataGridView1_KeyDown( INPUT sender AS System.Object, INPUT e AS...
   CASE e:KEYCODE:ToString():
      WHEN "Insert" THEN Cre:PerformClick().
      WHEN "Return" THEN Edit:PerformClick().
      WHEN "Delete" THEN Del:PerformClick().
   END CASE.
   RETURN.

END METHOD.

Cre, Edit, Del to nazwy przycisków odpowiednio dla: Create, Edit i Delete. Całość działa poprawnie.

I już naprawdę na koniec, dodamy licznik rekordów. Przyda się gdy będziemy dodawać czy usuwać rekordy.
Dodajemy obiekt typu label, nadajemy nazwę np. labInfo. W metodzie refreshData dodajemy na końcu linię kodu:

labInfo:TEXT = "Liczba rekordów: " + STRING(bsCust:Count).

Po każdym wywołaniu tej metody liczba rekordów jest wyświetlana w obiekcie.

Niniejszym kończymy na razie cykl tworzenia aplikacji przy wykorzystaniu obiektów .Net w ABL, ale być może do niego wrócimy. Ponieważ pojawiła się już wersja OpenEdge 12, teraz trzeba napisać coś na ten temat.

Wiesław Kurzątkowski
Piotr Tucholski

Tworzenie obiektowego UI w OE Development Studio cz. III

W poprzedniej części zbudowaliśmy aplikację składającą się z dwóch okienek (form) z browserami danych z bazy OpenEdge. Wszystkie te dane są do odczytu; ich edycję omówimy niedługo, ale jeszcze nie w tym odcinku.

Na razie wracamy do ustawień, związanych z wyświetlaniem danych w DataGrid. Tych parametrów jest bardzo dużo. Omówimy tylko wybrane, zostawiając Wam pole do własnych badań.


W parametrach DatGridView formy fOrder, parametr: AutoSizeColumnsMode – ustawmy na wartość: Fill.


W oknie widzimy, że kolumny są ciasno upakowane. Ponieważ jednak to .Net i mamy odpowiednio ustawiony parametr Anchor więc po rozciągnięciu okna mamy widok:

Można te dane jeszcze bardziej ścieśnić ustawiając wartość: DisplayedCellsExceptHeader. Etykiety kolumn mogą jednak nie być wyświetlone w całości.

AutoSizeRowsMode – wpływa na automatyczne ustawienie wysokości wiersza. Przy pomocy tego parametru możemy np. wyświetlić łamane wartości pola.

Przechodzimy teraz do formy fCustomer. Dodajmy do DataGrid pole Comments. Ustawmy AutoSizeRowsMode na wartość DisplayedCells – wysokość wiersza będzie dobrana po analizie rekordów do wyświetlenia. Możemy ustawić również wartość AllCells, ale w przypadku dużej liczby rekordów analiza może trwać o wiele dłużej.


Wchodzimy do edytora kolumn: Zadania DataGridView -> Edytuj kolumny… (edytor jest także dostępny pod widokiem z właściwościami).

Zaznaczamy pole Comments. W sekcji Wygląd wybieramy parametr DefaultCellStyle: DataGridViewCellStyle …

Ustawmy WrapMode na True oraz jakiś kolor tła – parametr BackColor.

Pole Comments jest już wyświetlane z zawijaniem wierszy.

A teraz pokażemy jak dodać dodatkowe pole do wyświetlenia w DataGrid, którego nie ma w bazie danych, a które bazuje na wartościach innych pól; w Progress ABL było ono znane jako calculated field.
Ponownie wchodzimy do Zadania DataGridView, ale tym razem klikamy Dodaj kolumnę…
Dodatkowa kolumna będzie zawierać sklejone wartości pól City i Country. Zaznaczamy opcję Kolumna niezwiązana z danymi i wypełniamy jak na rysunku poniżej.

Klikamy Dodaj.
W edytorze kolumn ustawiamy nową kolumnę CityCountry zaraz za polem Country.

Na razie w nowym polu nie ma żadnych danych.

Musimy teraz włączyć tryb wirtualny (VirtualMode) – wskazuje on czy podano własne operacje zarządzania danymi dla DataGridView.
Teraz trzeba włączyć obsługę odpowiedniego zdarzenia. Wg Wiesława można to zrobić na kilka sposobów, jednakże dobrym wyborem, ze względu na wydajność, jest zdarzenie CellValueNeeded. Jest ono wywoływane tylko gdy wartość komórki jest potrzebna np. do wyświetlenia czy przekazania wartości.

W kodzie zostaje wstawiona metoda obsługująca to zdarzenie. Poprzez parametr e mamy dostęp do argumentów zdarzenia.
W kodzie metody dodajemy prosty kod, dla przetestowania, czy idziemy właściwą drogą: e:Value = “OK”.

METHOD PRIVATE VOID dataGridView1_CellValueNeeded( INPUT sender AS System.Object, INPUT e AS System.Windows.Forms.DataGridViewCellValueEventArgs ):
       e:Value = "OK".
       RETURN.
END METHOD.

Na razie wszystko idzie zgodnie z planem.

Musimy teraz znaleźć nazwy obu kolumn w DataGridView, które zawierają dane dla City i Country.

METHOD PRIVATE VOID dataGridView1_CellValueNeeded( INPUT sender AS System.Object, INPUT e AS System.Windows.Forms.DataGridViewCellValueEventArgs ):
       e:Value = dataGridView1:Rows[e:RowIndex]:Cells[2]:Value:ToString() + ", " +
       dataGridView1:Rows[e:RowIndex]:Cells[3]:Value:ToString()..
       RETURN.
END METHOD.

W powyższej instrukcji wartości kolumn pochodzą z trzeciej i czwartej kolumny w gridzie (liczone 0,1,2,3…).
Zmiana kolejności kolumn w gridzie spowoduje błędne wyświetlanie w naszej dodatkowej kolumnie dlatego lepiej jest wstawienie wartości kolumn po nazwie. W deklaracji zmiennych możemy znaleźć:

DEFINE PRIVATE VARIABLE cityDataGridViewTextBoxColumn AS System.Windows.Forms.DataGridViewTextBoxColumn NO-UNDO.
.....................
DEFINE PRIVATE VARIABLE countryDataGridViewTextBoxColumn AS System.Windows.Forms.DataGridViewTextBoxColumn NO-UNDO.

Obsługę zdarzenia możemy zatem zapisać następująco:

METHOD PRIVATE VOID dataGridView1_CellValueNeeded( INPUT sender AS System.Object, INPUT e AS System.Windows.Forms.DataGridViewCellValueEventArgs ):
   e:Value = dataGridView1:Rows[e:RowIndex]:Cells["cityDataGridViewTextBoxColumn"]:Value:ToString() + ", " +
   dataGridView1:Rows[e:RowIndex]:Cells["countryDataGridViewTextBoxColumn"]:Value:ToString().
   RETURN.
END METHOD.

W nowej kolumnie dane wyglądają, tak jak to sobie zaplanowaliśmy. teraz trzeba tylko wyłączyć kolumny City i Country.

W edytorze kolumn ustawiamy wartość parametru Visible na False.

Teraz jest OK.

Dziękujemy za uwagę.

Wiesław Kurzątkowski
Piotr Tucholski

Tworzenie obiektowego UI w OE Development Studio cz. II

Kontynuujemy budowę aplikacji rozpoczętą w pierwszej części.

Mamy formę fCustomer; teraz zbudujemy drugą formę fOrder, która będzie zawierała browser (grid) z rekordami Order dla danego Customera.

Postępujemy analogicznie jak w poprzedniej części: do nowej formy wstawiamy źródło danych binding source, zmieniamy nazwę na np. bsOrder. Definiujemy schemat dla bsOrder.

Wstawiamy DataGridView wybierając źródło danych bsOrder.

Ponieważ browser ma pokazywać tylko rekordy Order dla zadanej wartości pola CustNum (nr klienta) trzeba przekazać z nadrzędnego okna (formy) parametr. Można to zrobić np. definiując w fOrder właściwość iCustNum.

    DEFINE PUBLIC PROPERTY iCustNum AS INTEGER NO-UNDO 
    GET.
    SET.

Metoda refreshData będzie tutaj wyglądała nieco inaczej niż w fCustomer.

    METHOD PUBLIC VOID refreshData(  ):
        
        def var bsOrdQuery as handle no-undo.
        def var bsOrdString as char no-undo.
        create query bsOrdQuery.
        bsOrdQuery:ADD-BUFFER (buffer order:handle).
        bsOrdQuery:QUERY-PREPARE ("for each order where order.custnum = "
                                   + string(iCustNum) + " by custnum").
        bsOrdQuery:QUERY-OPEN ().
        bsOrder:handle = bsOrdQuery.
        RETURN.

    END METHOD.

Wywołanie refreshData wstawiamy do kodu programu zgodnie z sugestiami z poprzedniej części.

Na koniec dodajemy do formy przycisk btnClose z etykietą Close. Definiujemy metodę dla zdarzenia Click wstawiając jedną instrukcję:

    METHOD PRIVATE VOID btnClose_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
        THIS-OBJECT:Close( ).
	RETURN.

    END METHOD.

Teraz przechodzimy do formy fCustomer. Wstawiamy deklarację zmiennej oOrd.

    DEF VAR oOrd AS fOrder. 

Definiujemy metodę dla zdarzenia Click przycisku btnOrders.

    
    METHOD PRIVATE VOID btnOrders_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
        IF (NOT VALID-OBJECT(oOrd) OR oOrd:IsDisposed) THEN DO:
            oOrd = NEW fOrder().
            oOrd:iCustNum = bscust:InputValue["CustNum"].
            oOrd:RefreshData().
            oOrd:Show().	//okno niemodalne

        END.
	RETURN.

    END METHOD.

Metoda tworzy instancję obiektu fOrder (najpierw sprawdza czy obiekt nie jest już zainicjowany), następie ustawia właściwość iCustNum (czyli przekazuje parametr), otwiera zapytanie i wyświetla formę w postaci niemodalnej. Oznacza to, że użytkownik ma dostęp zarówno do formy fOrder jak i fCustomer.

Gdybyśmy chcieli uruchomić fOrder w postaci modalnej (dostęp do fCustomer będzie dopiero po zamknięciu fOrder) to zamiast instrukcji oOrd:Show() trzeba napisać: WAIT-FOR oOrd:ShowDialog().

Okno z zamówieniami otwiera się przykrywając częściowo okno z klientami. Można zdefiniować parametry okna tak aby otwierało się np. tuż obok. Aby to osiągnąć trzeba skorzystać z poniższej sztuczki Wiesława:

    oOrd:Location = new System.Drawing.Point(int(this-object:Location:X) + 
        this-object:Width,int(this-object:Location:Y)).
    oOrd:Height = this-object:Height.

Cały tryger ma teraz następującą postać:

METHOD PRIVATE VOID btnOrders_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
    IF (NOT VALID-OBJECT(oOrd) OR oOrd:IsDisposed) THEN DO:
        oOrd = NEW fOrder().
        oOrd:iCustNum = bscust:InputValue["CustNum"].
        oOrd:RefreshData().
        oOrd:Show().	//okno niemodalne
        //WAIT-FOR oOrd:ShowDialog().	//okno modalne
        oOrd:Location = new System.Drawing.Point(int(this-object:Location:X) + 
           this-object:Width,int(this-object:Location:Y)).
        oOrd:Height = this-object:Height.
    END.
	RETURN.

END METHOD.

Efekt widać poniżej:

Naszą aplikację trzeba jeszcze trochę usprawnić. Ponieważ zdecydowaliśmy się na okno niemodalne, trzeba obsłużyć zmianę wyświetlanego rekordu w oknie fCustomer – powinna temu towarzyszyć zmiana wyświetlanych danych w oknie fOrder.
W widoku zdarzeń bsCust klikamy na zdarzenie PositionChanged (innym zdarzeniem, które warto wziąć pod uwagę jest CurrentChanged).

Do kodu programu zostanie wstawiona metoda obsługująca to zdarzenie. Musimy dodać do niej poniższy kod:

METHOD PRIVATE VOID bsCust_PositionChanged( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
    IF VALID-OBJECT(oOrd) THEN DO:
        IF oOrd:IsDisposed THEN  
           RETURN.                                                              
        oOrd:iCustNum = bscust:InputValue["CustNum"].
        oOrd:RefreshData().
    END.	
    RETURN.

END METHOD.

Możecie teraz sprawdzić, że jeśli okno fOrder jest otwarte, zmiana podświetlonego rekordu w oknie fCustomer powoduje wyświetlenie zamówień dla tego rekordu.

To oczywiście nie koniec. Wkrótce pojawi się następna część.

Wiesław Kurzątkowski
Piotr Tucholski

Tworzenie obiektowego UI w OE Development Studio cz. I

Nieco ponad 2 lata temu pokazałem jak zrobić pierwsze kroki na drodze tworzenia interfejsu graficznego z wykorzystaniem obiektów .Net w narzędziu OpenEdge Developer Studio.
Temat ten w rozmowach z Wami co jakiś czas powraca. Powodem jest najczęściej chęć wykonania interfejsu o lepszym wyglądzie i bardziej skalowalnego niż tradycyjny, niemłody już interfejs progressowy.

Do powrotu skłoniło mnie ostatecznie spotkanie z Wiesławem Kurzątkowskim – specjalistą od programowania z wykorzystaniem technik obiektowych firmy Novum. Wiesław obiecał pokazać niektóre stosowane przez siebie rozwiązania oraz służyć swoją wiedzą fachową w razie pytań i wątpliwości.

W dokumentacji OpenEdge można znaleźć ciekawe przykłady całych aplikacji, ale zwracacie uwagę na to, że po pierwsze: nie tworzy się ich od podstaw, a z półgotowych projektów, po drugie: opierają się na kontrolkach Infragistics, na które trzeba mieć dodatkową licencję. W naszych przykładach wykorzystamy ogólnodostępne kontrolki Microsoft oraz OpenEdge. Jeśli będzie zainteresowanie z Waszej strony to także pokażemy coś z kontrolkami Telerik.

OK, rozpoczynamy od powtórzenia stworzenia prostej aplikacji, opisanej we wspomnianym artykule, przedstawiającej browser (grid) z rekordami z tabeli Customer, a następnie dodamy drugie okno zawierające grid z rekordami Orders dla danego Customera.

W narzędziu OpenEdge Developer Studio 11.7 zakładamy nowy projekt o nazwie net1 typu klienckiego GUI for .NET.

Do projektu przyłączamy połączenie z serwerem bazy sports2000 (opisywane już wcześniej). Teraz tworzymy nową formę New -> ABL Form o nazwie fCustomer, naciskamy Finish.

Klasę formy można podglądać w postaci kodu ABL lub wizualnej (OE Visual Designer). Między trybami możemy się przełączać np. poprzez F9 / Shift + F9.

Do pustej formy przeciągamy z zestawu kontrolek OpenEdge: ProBindingSource. W widoku Properties zmieniamy nazwę domyślną na bsCust.

Następnie definiujemy schemat dla bsCust wybierając połączenie z bazą danych. Wybieramy tabelę Customer i kilka pól, jak na rysunku.

Teraz dodajemy kontrolkę DataGridView, wybierając źródło danych binding source bsCust i ew. ustawiając kolumny w żądanej kolejności.

W oknie wstawiamy dwa przyciski (Microsoft Button): btnOrders z etykietą Orders i btnClose z etykietą Close, jak na rysunku. Uruchomienie naszej aplikacji nie spowoduje jeszcze wyświetlenia danych Customer. Trzeba w tym celu napisać krótki fragment kodu.

Dodajemy metodę refreshData, która definiuje i otwiera zapytanie dla tabeli Customer, a następnie podpina je do źródła danych bsCust.

    METHOD PUBLIC VOID refreshData(  ):
        
        DEF VAR bsCustQuery AS HANDLE NO-UNDO.
        DEF VAR bsCustString AS CHAR NO-UNDO.
        CREATE QUERY bsCustQuery.
        bsCustQuery:ADD-BUFFER (BUFFER customer:HANDLE).
        bsCustQuery:QUERY-PREPARE ("FOR EACH customer").
        bsCustQuery:QUERY-OPEN ().
        bsCust:HANDLE = bsCustQuery.
        RETURN.

    END METHOD.

Wywołanie tej metody dodajemy do konstruktora.

    CONSTRUCTOR PUBLIC fCustomer (  ):
             
        SUPER().
        InitializeComponent().
        THIS-OBJECT:ComponentsCollection:ADD(THIS-OBJECT:components).
        refreshData().
        
        CATCH e AS Progress.Lang.Error:
            UNDO, THROW e.
        END CATCH.

    END CONSTRUCTOR.

Uruchamiamy aplikację – dane z tabeli Customer są poprawnie wyświetlane w browserze.

Wywołanie refreshData w konstruktorze może w niektórych przypadkach bardziej złożonych aplikacji być niekorzystne i generować błędy. To samo osiągniemy wstawiając wywołanie refreshData w metodzie formy Load lub Shown. Klikamy więc dwukrotnie w widoku Events na Shown; automatycznie jest wstawiona do naszego kodu metoda obsługi dla tego zdarzenia. Przenosimy tutaj wywołanie refreshData z konstruktora.

    METHOD PRIVATE VOID fCustomer_Shown( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
        refreshData().	
	RETURN.

    END METHOD.

W obiektach .NET jest wiele predefiniowanych ustawień i zdarzeń, które możemy wykorzystywać dla poprawienia funkcjonalności naszych aplikacji. Jednym z nich jest automatyczne sortowanie AutoSort dla binding source. Zmieniamy wartość tego parametru na True.


Jeśli klikniemy teraz na nazwę kolumny, uzyskamy sortowanie danych w browserze według jej wartości. Poniżej widać sortowanie po polu Name.

Na koniec tej części pokażemy ustawienia parametru Anchor (zakotwiczenie) i jak on wpływa na skalowalność obiektów.

Domyślna wartość Anchor to Top, Left. Wartości te powodują, że obiekt nie jest dobrze skalowalny gdy zmieniamy rozmiar okna pociągając za prawą lub dolną krawędź. Na obrazku poniżej, browser nie zwiększył automatycznie wysokości, a prawy przycisk jest niewidoczny.

Na tym rysunku poszerzenie okna nie powoduje zwiększenie szerokości browsera.

Zmieńmy ustawienia Anchor dla browsera na: Top, Bottom, Left, Right

… a dla przycisków na: Bottom, Right. Uruchamiamy znów aplikację…

Zmiana rozmiarów okna powoduje automatyczne dostosowanie rozmiaru browsera, a przyciski pozostają widoczne pod browserem.

Rozszerzenie okna powoduje wprawdzie odsłonięcie szarego obszaru, ale tylko dlatego, że nie ma więcej kolumn do wyświetlenia. Chyba fajnie, co?

Na razie to tyle. Następnym razem dodamy okno dla zamówień aktualnego klienta.

Wiesław Kurzątkowski
Piotr Tucholski

Partycje Tabel OpenEdge cz. III

Zanim przejdę do kolejnej części związanej z partycjami tabel, winien jestem odpowiedzi na pytanie związane z definiowaniem zasad partycjonowanie.
Otóż kilku z Was chciało wiedzieć czy jest planowana możliwość definiowania podziału danych w tabeli w oparciu nie o wartość pola, lecz o funkcję (np. YEAR(TODAY)-1).
Miałem okazję niedawno rozmawiać z Richem Banvill’em – odpowiedzialnym za rozwój bazy OpenEdge. Taka możliwość nie jest w planach, ani w wersji OE12 ani w dalszej przyszłości.
Rich wspomniał, że problem ten można rozwiązać np. poprzez zdefiniowanie zadań w cronie.

Wróćmy jednak do tematu. W poprzedniej części zakończyliśmy podział tabeli w oparciu o wartości pola Country (lokalny idex). Wspomniałem, że partycjonowanie jest transparentne dla aplikacji, jednakże mogą zdarzyć się sytuacje gdy zapytanie zwraca nie te same rekordy co przed partycjonowaniem. Jednym z przykładów jest stosowanie w kodzie wyszukiwania po adresach RECID/ROWID. Ponieważ partycjonowanie przenosi rekordy, ich fizyczne adresy ulegają zmianom. W przypadku stosowania tego rozwiązania trzeba wprowadzić odpowiednie poprawki w kodzie aplikacji.

Innym, częstszym przykładem jest stosowanie w zapytaniu filtra na dane pole (WHERE). Ponieważ w partycjonowanej tabeli dodany jest zazwyczaj nowy, lokalny index, wyszukiwanie może być realizowane w oparciu o niego. Aby mieć pewność, że index nie został zmieniony i aby zachować ten sam porządek rekordów trzeba ew. dopisać frazę USE-INDEX.

Teraz trzeba wspomnieć o ważnej funkcji technologi partycjonowania tzw. pruning (okrajanie partycji).

Proces pruningu analizuje automatycznie zapytanie i (o ile to możliwe) nie bierze pod uwagę rekordów w partycjach, które nie spełniają warunków zapytania. Na rysunku widać przykład podziału rekordów na kwartały wg pola OrderDate. Jeśli w zapytaniu warunki będą dotyczyły rekordów tylko z kwartału drugiego, to tylko ta jedna partycja będzie w użyciu. Technologia ta może znacząco wpłynąć na poprawę wydajności.

Następnym zagadnieniem jest stosowanie frazy TABLE-SCAN. Powoduje ona pobranie rekordów bez dostępu do indexów i wyświetlenie ich w porządku, w jakim znajdują się w blokach bazy. Porządek rekordów zmieni się więc po partycjonowaniu, ze względu na przeniesienie ich do innych bloków. Przed pobieraniem wykonywana jest operacja pruning. Zobaczmy poniższy przykład:

FOR EACH order WHERE country = “USA” AND
  OrderDate >= 10-01-2014 AND
  OrderDate <= 12-31-2014 TABLE-SCAN:
DISPLAY OrderNum...

Jak widać, partycjonowanie tabel może wpłynąć na porządek przetwarzanych rekordów. Zobaczmy teraz jak wygląda sytuacja z tworzenie nowych rekordów i ich edytowaniem.

Należy pamiętać, że rekordy muszą mieć określone wartości dla pól, wg których odbywa się partycjonowanie. Dlatego warto stosować instrukcję ASSIGN grupującą ustawienia wartości dla rekordu, zaraz po instrukcji CREATE. Instrukcja ASSIGN wymusza fizyczne utworzenie rekordu w bazie, a więc zadziałanie mechanizmu portycjonowania. Poniższy przykład wywoła błąd (partycjonowanie po polu Country).

CREATE order.
ASSIGN ordernum = NEXT-VALUE(next-ord-num). /* ERROR! */
ASSIGN country = “USA”. 

Przykład ten łatwo poprawić do postaci:

CREATE order.
ASSIGN ordernum = NEXT-VALUE(next-ord-num)
       country = “USA”.

Edycja pola rekordu, wg którego realizowany jest podział tabeli niesie ze sobą przeniesienie całego rekordu do innego obszaru, a więc skasowanie rekordu, utworzenie go w nowym obszarze, aktualizacja indexów. Zbyt częste takie operacje mogą nieść ze sobą obniżenie wydajności. Może to być wynikiem niewłaściwie wybranego klucza partycji, co warto przedyskutować i ew. wybrać inny klucz.

 

 

Partycje Tabel OpenEdge cz. II

W poprzedniej części zapoznaliśmy się nieco z częścią teoretyczną, dotyczącą partycjonowania tabel OpenEdge. Teraz czas zrobić coś w praktyce.

Tworzymy nową bazę np. mytp, kopię bazy sports2000 :

prodb mytp sports2000

Do partycjonowania tabel dane muszą znajdować się w obszarach Typ II. Demonstracyjna baza sports2000 posiada tylko obszary Typ I. Dodajemy zatem obszary, do których przeniesiemy tabele Customer (Cust_Data2) i Order (Order2), a także  obszary, w których będą przechowywane dane po procesie partycjonowania.

Najpierw tworzymy plik add_tp.st

d "Cust_Data2":20,64;8 . f 1280
d "Cust_Data2":20,64;8 .
#
d "Cust_Index2":25,32;8 . f 1280
d "Cust_Index2":25,32;8 .
#
d "Order2":30,64;8 . f 1280
d "Order2":30,64;8 .
#
d "CustomerData":300,64;8 ./customer f 1280
d "CustomerData":300,64;8 ./customer
d "CustomerIndex":301,32;8 ./customer f 320
d "CustomerIndex":301,32;8 ./customer
#
d "OrderData":400,64;8 ./order f 1280
d "OrderData":400,64;8 ./order 
d "OrderIndex":401,32;8 ./order f 320
d "OrderIndex":401,32;8 ./order
#

W katalogu z bazą tworzymy podkatalogi customer i order.
Teraz uruchamiamy polecenie:

prostrct add mytp add_tp.st

Przenosimy tabele Customer i Order do nowych obszarów Typ II:

proutil mytp -C tablemove customer Cust_Data2

proutil mytp -C tablemove order  Order2

Niestety, musimy także przenieść wszystkie indexy do obszarów Typ II (nie będę tego tutaj pokazywać, ale to też proste zadanie). Bazę demo można przygotować na różne sposoby, np. kasując niepotrzebne obiekty lub dump i load do nowej struktury.

Podobnie jak w przypadku CDC, dodamy konfigurację serwera bazy w OE Management (lub OE Explorer).  Po zalogowaniu się wybieramy Resources -> Database. Pojawia się widok Database Migration Utility, w którym podajemy parametry utworzonej bazy mytp wraz z numerem portu, np. 1009. Zaznaczamy Autostart database broker.

Na górnej listwie OE Management (OE Exlporer) klikamy Database Administration i na nazwę bazy: mytp. Pojawia się poniższy ekran.

W sekcji Database Features klikamy Table Partitioning Enable

… i jeszcze w Enable table partitioning. Funkcja jest już włączona w bazie.

W słowniku baz danych dodajemy dla tablicy Order index: OrderDateLocal, obszar: OrderIndex, pole: OrderDate. Analogicznie dla tablicy Customer index: CustomerCountryLocal, obszar: CustomerIndex, pole: Country.

Wracamy do OE Explorera. W sekcji Storage Management klikamy Create partition policy.

Pojawia się poniższy ekran, w którym definiujemy ustawienia dla naszej pierwszej partycji dla tablicy Order. Klikamy Create partition policy. Tworzymy partycję dla tablicy Customer.

Wypełniamy dane jak powyżej (w lookupach wyświetlają się tylko tablice w obszarach Typ II), zaznaczamy: Immediate – set new partitions to allocate space. Klikamy Next.

Będzie to partycja typu List, więc NIE zaznaczamy opcji Has range. Klikamy Add fields from index, wybieramy nowo utworzony index CustomerCountryLocal. Partycja podzieli dane wg pola Country.

Klikamy Next oraz Load Details. Otrzymujemy info, że znaleziono 9 podobszarów dla naszej partycji (jest to związane z wartościami pola Country). Klikamy Next.

Widzimy szczegóły Table Partition Policy. Klikamy Finish.

Zostaje utworzona nowa zasada o nazwie Customer. Dane w tablicy nie są jednak jeszcze podzielone.

Przygotowujemy dane do migracji. Klikamy w Edit Details wchodząc ponownie w szczegóły zasady partycjonowania. Klikamy podwójnie w każdy szczegół np. Austria i zaznaczamy Split-target. Tylko te zaznaczone elementy zostaną podzielone. Oznaczam Split-target wszystkie 9 elementów (czyli dla wszystkich krajów).


Wybieram Commit. Wszystko jest już przygotowane aby przenieść dane z partycji złożonej (Composite) do partycji podzielonych (Split-target).

W oknie komend proenv wpisujemy polecenie:

proutil mytp -C partitionmanage split table customer composite initial useindex

CustomerCountryLocal recs 100

Dane zostały podzielone przy użyciu zdefiniowanego indexu lokalnego. Ilość rekordów w transakcji: 100.

Możemy sprawdzić gdzie znajdują się teraz dane z tablicy Customer uruchamiając dobrze znane polecenie: proutil mytp -C tabanalys

Analiza dla obszaru CustomerData pokazuje podział tablicy na 9 partycji.

To na razie tyle w niniejszym odcinku, ale to nie koniec tematu partycjonowania.

Partycje Tabel OpenEdge cz. I

W ostatnim czasie kilka osób prosiło mnie o wyjaśnienie na czym polega partycjonowanie tabel w bazach OpenEdge. Ten temat czasem powraca w rozmowach z Wami, więc postanowiłem go nieco przybliżyć.

Partycjonowanie tabel (Table Partitioning) to oddzielny produkt dla baz Enterprise, umożliwiający dzielenie dużych tabel na mniejsze części pod względem logicznym zwane partycjami.

Partycje są zaimplementowane na poziomie bazy danych i przezroczyste dla aplikacji. Korzystanie z partycjonowanych tabel może wymagać niewielkich zmian w kodzie aplikacji lub nie wymagać ich wcale.

Uwaga: obiekty podzielone na partycje muszą znajdować się w storage area Typ II.

Partycjonowanie posiada kilka istotnych cech. Partycje w tabeli są niezależne, więc można realizować do nich jednoczesny dostęp, poprawiając wydajność zapytań. Jeśli jedna partycja w tabeli nie jest dostępna, pozostałe partycje są nadal dostępne dla aplikacji.


Każdy rekord tabeli podzielonej na partycje ma te same kolumny.
Każdy rekord może znajdować się tylko w jednej partycji.
Każda partycja może znajdować się we własnym obszarze przechowywania (storage area),
Każda partycja może być niezależnie modyfikowana i zarządzana bez wpływu na inne partycje tej samej tabeli.


Ponadto:
indeksy związane z tabelami podzielonymi na partycje można również podzielić na partycje (tzw. lokalne indeksy).
Podobnie jak partycje tabel, każdy lokalny indeks może znajdować się we własnym obszarze przechowywania i być niezależnie zarządzany. Oprócz indeksów lokalnych mogą istnieć także indeksy globalne dla wszystkich danych w tabeli, bez względu na wydzielone partycje.

Kiedy warto stosować tę technologię?

Np. dla tabel zawierających dane historyczne, które muszą być
archiwizowane. Ciekawą cechą partycji jest to, że jeśli jakaś partycja zawiera dane, które nie mogą być modyfikowane, może być ustawiona tylko do odczytu. W ten sposób w tabeli można modyfikować tylko dane aktualne, a nie zarchiwizowane.

Inne przykładowe przyczyny:

Tabele zawierające dane, które muszą być rozłożone na różnych urządzeniach pamięci masowej.
Duże tabele, które muszą podlegać okresowym operacjom na rekordach i indeksach.
Tabele zawierają kolumny, wg których można logicznie pogrupować dane.
Tabele zawierające dane z częstymi zapytaniami z frazą TABLE-SCAN, a nie WHOLE-INDEX.
Tabele, które będą rosły do bardzo dużych rozmiarów.

Partycjonowanie wykorzystuje jedną kolumnę (tzw. partition key) do jednoznacznej identyfikacji każdej partycji. Podział może być według zakresu (np. zakres dat) lub według listy (lista odrębnych wartości kolumn, np. kraje, województwa, itp.). Kolumna partition key nie może mieć wartości nieokreślonych (“?”).

Należy dodać, że dane w partycji można podzielić na dalsze partycje (możliwe jest 15 poziomów podziału). Np. dzielimy zamówienia (tablica order) według zakresu dat, a następnie każdą partycję dzielimy dodatkowo na subpartycje wg kraju.

 

Sposób w jaki tabele są podzielone jest opisane w tzw. zasadach partycjonowania (partition policy), znajdujących się w meta-schemacie bazy.

W następnym odcinku pokażę jak w praktyczny sposób stworzyć partycje i przenieść do nich dane.

PASOE i serwisy WebHandler cz. II

Reasumując to co można było prześledzić w poprzednim artykule, warstwa transportowa WEB jest związana z interfejsem biznesowym podobnym do REST. Wykorzystuje te same żądania (czasowniki) HTTP (GET, PUT, POST,…). Istotna różnica to przechwytywanie obsługi żądań przez WebHandler, czyli klasę OO ABL. Dzięki temu unika się żmudnego mapowania, a deweloperzy dostają duże możliwości dopasowania obsługi do własnych potrzeb. Dostęp do serwisu jest przez URI, w którym zamiast warstwy REST podaje się WEB oraz nazwę WebHandlera, który obsługuje dany serwis np: webh1/web/webh1.
Webh1 jest tutaj nazwą serwisu, WEB wartswą transportową, a następne wystąpienie webh1, WebHandlerem (domyślny WebHandler ma tę samą nazwę jak serwis).

W drugim przykładzie (także opartym na bazie wiedzy) pokażę jak zdefiniować w projekcie/serwisie dodatkowe URI i jak zrealizować dostęp do 2 obiektów business entity.

Utwórzmy kolejny projekt webh2, typ Server i warstwa transportowa WEB. Projekt jest połączony z baza sports2000. Kto nie wie jak to zrobić niech zajrzy do części I.

Definiujemy business entity: customerBE dla tablicy customer i orderBE dla tablicy order. Zostają utworzone dwie klasy customerBE.cls oraz orderBE.cls (a także pliki .i) – patrz poniżej.

Teraz klikamy prawym klikiem myszy na nazwę serwisu i wybieramy Edit oraz przycisk Add.


Dodajemy URI: /Customer oraz /Orders.

Modyfikujemy plik klasy webh2Handler.cls aby miał następującą postać (dokładniej, modyfikowana jest metoda HandleGet):

USING Progress.Lang.*.
USING OpenEdge.Web.WebResponseWriter.
USING OpenEdge.Net.HTTP.StatusCodeEnum.
USING OpenEdge.Web.WebHandler.
USING Progress.Json.ObjectModel.*.
USING customerBE.*.
USING orderBE.*.  

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS webh2Handler INHERITS WebHandler: 

 {"customerbe.i"}
 {"orderbe.i"}   	
		
	/*------------------------------------------------------------------------------
            Purpose: Handler for unsupported methods. The request being serviced and
            		 an optional status code is returned. A zero or null value means 
            		 this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotAllowedMethod( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
		/* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */
        	
		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
	END METHOD.


	/*------------------------------------------------------------------------------
            Purpose: Handler for unknown methods. The request being serviced and an 
                     optional status code is returned. A zero or null value means 
                     this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotImplemented( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
		/* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */	
		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
   	END METHOD.
 	
	
	/*------------------------------------------------------------------------------
            Purpose: Default handler for the HTTP GET method. The request being 
                     serviced and an optional status code is returned. A zero or 
                     null value means this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
 	METHOD OVERRIDE PROTECTED INTEGER HandleGet( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
 	
	
		DEFINE VARIABLE oResponse AS OpenEdge.Net.HTTP.IHttpResponse NO-UNDO.
        DEFINE VARIABLE oWriter   AS OpenEdge.Web.WebResponseWriter  NO-UNDO.
        DEFINE VARIABLE oBody     AS OpenEdge.Core.String            NO-UNDO.
       
        DEFINE VARIABLE jsonObj AS JsonObject NO-UNDO.
        DEFINE VARIABLE lcJSON AS LONGCHAR NO-UNDO.

        ASSIGN 
            oResponse            = NEW OpenEdge.Web.WebResponse()
            oResponse:StatusCode = INTEGER(StatusCodeEnum:OK).
        

        jsonObj = NEW JsonObject().
        
        IF ENTRY(2,poRequest:PathInfo,"/") = "Customer" THEN
        DO: 
            DEFINE VARIABLE beCustomer AS customerBE NO-UNDO.
            DEFINE VARIABLE hTTCustomer AS HANDLE NO-UNDO.
            
            beCustomer = NEW customerBE().
            beCustomer:ReadcustomerBE("",OUTPUT DATASET dsCustomer).
            hTTCustomer = TEMP-TABLE ttCustomer:HANDLE.
            hTTCustomer:WRITE-JSON ("JsonObject",jsonObj).
            lcJSON= jsonObj:GetJsonText().
            oBody = NEW OpenEdge.Core.String(lcJSON).
                      
            ASSIGN    
                oResponse:Entity = oBody
                oResponse:ContentType   = 'application/json':u
                oResponse:ContentLength = oBody:Size.
        END.
        ELSE
           IF ENTRY(2,poRequest:PathInfo,"/") = "Orders" THEN
           DO:
            DEFINE VARIABLE beOrder AS orderBE NO-UNDO.
            DEFINE VARIABLE hTTOrder AS HANDLE NO-UNDO.
            
            beOrder = NEW orderBE().
            beOrder:ReadOrderBE("",OUTPUT DATASET dsOrder).
            hTTOrder = TEMP-TABLE ttOrder:HANDLE.
            hTTOrder:WRITE-JSON ("JsonObject",jsonObj).
            lcJSON= jsonObj:GetJsonText().
            oBody = NEW OpenEdge.Core.String(lcJSON).
                      
            ASSIGN 
                oResponse:Entity = oBody
                oResponse:ContentType   = 'application/json':u
                oResponse:ContentLength = oBody:Size.
          END.
          ELSE
          DO:
            ASSIGN
                oBody = NEW OpenEdge.Core.String(
                                 'Hello '
                               + '~r~n':u   /*CRLF */
                               + 'This is the default message by th HandleGet in webh2Handler.'
                               ).
            ASSIGN
                oResponse:Entity        = oBody
                oResponse:ContentType   = 'text/plain':u
                oResponse:ContentLength = oBody:Size.                               
         END.
       
        ASSIGN 
            oWriter = NEW WebResponseWriter(oResponse).
        oWriter:Open().
        
        oWriter:Close().
        
        RETURN 0.
		
 	END METHOD. 
 	 	   	
	
END CLASS.

Po uruchomieniu serwera PASOE możemy przetestować działanie serwisu.
Wpisujemy w przeglądarce URL dla danych z tablicy Customer: http://localhost:8810/webh2/web/Customer.

Następnie dla Orders: http://localhost:8810/webh2/web/Orders.

A na końcu testujemy domyślny WebHandler: http://localhost:8810/webh2/web/webh2.

Jeszcze wspomnę, że dodawać WebHandlery i przypisywać do nich URI można na różne sposoby.
Jednym z nich, choć niepolecanym, jest plik konfiguracyjny instancji serwera aplikcaji openedge.properties (podkatalog conf).

O wiele lepszym sposobem jest interfejs webowy OpenEdge Explorer.
Wybieramy: Progress Application Server -> oepas1 -> ABL Application: oepas1 -> ABL WebApp: webh2 -> WEB Transport Configuration

1 2 3