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.

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

PASOE i serwisy WebHandler cz. I

Deweloperzy aplikacji webowych z wykorzystaniem serwera PASOE mają od wersji 11.6.3 nową możliwość tworzenia serwisów typu Data Object. Są to tzw. WebHandlery, które działają z wykorzystaniem warstwy transportowej WEB (Data Object WebHandler Service).

WebHandler zapewnia bardziej wydajną i prostszą warstwę komunikacyjną niż REST. Jest napisany w ABL, łatwiej dopasować go do własnych potrzeb i ma ulepszone możliwości debugowania. Podstawową zaletą WebHandlera jest to, że daje programistom pełną kontrolę nad przychodzącymi i wychodzącymi danymi.

Warstwa WEB obsługuje żądania (requests) i odpowiedzi (responses), które używają standardowych czasowników HTTP (verbs). Obejmuje to interakcje z klientami, takimi jak WebSpeed i OpenHTTP.
Jeśli chcesz zmienić domyślny adres URL, możesz dodać dodatkowe WebHandlery i zmapować je na różne adresy URL lub zmienić odwzorowania domyślnych WebHandlerów.

Dodatkowe WebHandlery można dodać w pliku openedge.properties dla instancji OEPAS i zmapować je na określone adresy URL. Na przykład:

defaultHandler=OpenEdge.Web.CompatibilityHandler
webhandler1=MyHandler:/mycustomer
webhandler2=MyHandler:/mycustomer/{custid}

OpenEdge.Web.CompatibilityHandler zapewnia kompatybilność z aplikacjami WebSpeed SpeedScript i CGI Wrapper. Jest to domyślny handler używany w instancji w środowisku programistycznym.

Tworzenie tych serwisów zilustruję dwoma przykładami zaczerpniętymi z bazy wiedzy.

Zaczynamy od założenia, że mamy instancję serwera PASOE z podłączoną bazą danych sports2000.
Tworzymy nowy projekt OpenEdge. Nadajemy mu nazwę np. webh1, wybieramy typ Server i zaznaczamy warstwę transportową WEB.

Klikamy Next, sprawdzając czy do projektu jest podłączona instancją serwera aplikacji, a w ostatnim kroku upewniamy się, że jest przyłączona nasza baza danych.

Kreator tworzy od razu klasę WebHandlera, w tym przypadku webh1Handler.cls, zawierającą 3 metody: HandleNotAllowedMethod, HandleNotImplemented oraz HandleGet. Dwie pierwsze są związane z wychwytywaniem błędów. HandleGet to główna metoda, w której można przetwarzać dane otrzymane z odpowiedzi (response).

Stworzymy teraz obiekt danych. W Project Explorerze zaznaczamy nazwę projektu i prawym klikiem myszy wybieramy New -> Business Entity.


Podajemy nazwę customerBE, klikamy Next i wybieramy z bazy tabelę customer.

W efekcie jest utworzona znana z poprzednich artykułów klasa customerBE.cls.
Do klasy tej będziemy odnosić się z WebHandlera, czyli pliku webh1Handler.cls. Teraz należy zmodyfikować metodę HandleGet aby miała postać tak jak w poniższej klasie webh1Handler.cls (można skopiować cały plik klasy).

Widać, że oprogramowanie metod wymaga znajomości programowania obiektowego i odpowiednich referencji, ale daje bardzo duże możliwości. W poniższym przykładzie w odpowiedzi HTTP pobierana jest zawartość tablicy ttCustomer (z obiektu customerBE) i wyświetlana poprzez obiekt JSON.

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

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS webh1Handler INHERITS WebHandler: 

	    {"customerbe.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 beCustomer AS customerBE NO-UNDO.
        DEFINE VARIABLE pcFilter AS CHAR NO-UNDO.
        DEFINE VARIABLE lcJSON AS LONGCHAR NO-UNDO.

        DEFINE VARIABLE jObj AS JsonObject.
        DEFINE VARIABLE htt AS HANDLE.
        
        jObj = NEW JsonObject().
        /* Get data from the BE */
        beCustomer = NEW customerBE().
        beCustomer:ReadcustomerBE(INPUT pcfilter,OUTPUT DATASET dsCustomer).
        htt = TEMP-TABLE ttCustomer:HANDLE.
        htt:WRITE-JSON("JsonObject", jObj).

        ASSIGN 
            oResponse            = NEW OpenEdge.Web.WebResponse()
            oResponse:StatusCode = INTEGER(StatusCodeEnum:OK)
                        .
        /* This body object can be a string or something else (JsonObject for instance) */
        lcJSON= jObj:GetJsonText().
        ASSIGN 
            oBody = NEW OpenEdge.Core.String(lcJSON).            
                              
        ASSIGN 
            oResponse:Entity        = oBody
            /* HTTP messages require a content type */
            oResponse:ContentType   = 'application/json':u
            /* ContentLength is good too */
            oResponse:ContentLength = oBody:Size
            .
        
        /* The WebResponseWriter ensures that the status line and
           all headers are writted out before the message body/entity. */
        ASSIGN 
            oWriter = NEW WebResponseWriter(oResponse).
        oWriter:Open().
        
        /* Finish writing the response message */
        oWriter:Close().
        
        /* A response of 0 means that this handler will build the entire response;
           a non-zero value is mapped to a static handler in the webapp's /static/error folder.
           The mappings are maintained in the webapps's WEB-INF/web.xml 
           A predefined set of HTTP status codes is provided in the OpenEdge.Net.HTTP.StatusCodeEnum 
           enumeration */
        RETURN 0.
		
 	END METHOD. 
 		
END CLASS.

W przeglądarce wpisujemu URL dla naszej instancji PASOE np.: http://localhost:8810/webh1/web/webh1 (lub https://localhost:8811/webh1/web/webh1 dla bezpiecznego połączenia).
W efekcie dostajemy ekran podzielony na 3 przekładki:
JSON

Raw Data

Headers