AI w Developers Studio

Nie byłem do końca pewien czy powinienem napisać ten artykuł, bo nie mogę pokazać efektu końcowego, ale ze względu na zainteresowanie AI w OpenEdge postanowiłem pokazać jak rozpocząć konfigurację ChatGPT w Developers Studio. Całość jest dość dobrze udokumentowana na stronach progressowych choć nie do końca.
Po pierwsze musimy mieć minimum wersję OE 12.8.4. Wersja ta umożliwia instalację wtyczki ChatGPT o nazwie AI Coding Assistant.
Wchodzimy na stronę Progress Electronic Software Download (ESD) (musimy oczywiście mieć konto), skąd pobieramy plugin AssistAI-ChatGPT-PDSOE-Plugin….zip, który rozpakowujemy na lokalnym dysku.

Teraz wchodzimy do Developers Studio i wybieramy Help -> Install New Software. Wypełniamy pola jak poniżej i przyciskamy Add .

Plugin AssistAI jest już na liście.

Zaznaczamy plugin, klikamy Next, zaznaczmy zgodę license agreement i klikając Finish rozpoczynamy instalację.
Na stronie Trust zaznaczamy Unsigned i Always trust all content.

Akceptujemy ryzyko i kończymy instalację. Developers Studio trzeba teraz zrestartować.

Widok ChatGPT mamy już na liście, ale to niestety nie koniec.

Jeśli otworzymy widok i wpiszemy jakieś zapytanie nie dostaniemy odpowiedzi.

Żeby podejrzeć błędy jakie generują się podczas naszych prób użycia ChatGPT warto otworzyć mało znany widok: Error Log ponieważ w widoku Problems nie znajdziemy błędów dla tego działania.


W logu znajdziemy informację, że potrzebny jest klucz, jaki możemy wygenerować rejestrując się na stronie OpenAI.

Rejestracja i wygenerowanie klucza dla darmowego konta jest bardzo proste. Następnym krokiem jest aktywacja pluginu. Wchodzimy w Windows -> Preferences -> AssistAI wypełniając pola, podając wygenerowany klucz.

Niestety, ChatGPT wciąż nie odpowiada. Sprawdzamy Error Log.

Aby móc korzystać z ChatGPT trzeba założyć konto z płatną subskrypcją.

Klient HTTP ABL 2

Kontynuując temat związany z budowaniem klienta HTTP w aplikacji ABL, napiszę krótko o najprostszym uwierzytelnianiu. Zabezpieczenia w serwerze PASOE opisałem w serii artykułów, a teraz przyda się odświeżenie podstawowych wiadomości zamieszczonych tutaj.
W lokalizacji dla web aplikacji …\webapps\myrest\WEB-INF znajduje się plik oeablSecurity.properties. Znajdujemy w nim wpis: client.login.model=anonymous i zmieniamy na basic. Wymusi to logowanie się do serwisu.

Sposób uwierzytelniania widoczny jest poprzez parametr http.all.authmanager=local oznaczający, że uwierzytelnienie jest lokalne w oparciu o użytkowników zdefiniowanych w pliku:
C:\WrkOpenEdge121\mysec\webapps\ROOT\WEB-INFF\users.properites.
Na pierwszej pozycji znajduje się wpis: restuser=password,ROLE_PSCUser,enabled. Zapamiętujemy nazwę użytkownika i hasło (możemy także zdefiniować własnych użytkowników).

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.
USING Progress.Json.ObjectModel.ObjectModelParser FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE iCount AS INTEGER NO-UNDO.
DEFINE VARIABLE oCreds AS Credentials NO-UNDO.


// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=4000").

// Create credentials
oCreds = NEW Credentials().
oCreds:UserName = "restuser".
oCreds:Password = "password".

// Build the request
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN DO:
    MESSAGE "Request Error " + STRING(OResponse:StatusCode).
    RETURN ERROR "Request Error: " + STRING(oResponse:StatusCode).
    END.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, TRUE).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

W sekcji // Create credentials budowany jest obiekt zawierający nazwę użytkownika i hasło:
oCreds = NEW Credentials(). W tym bardzo prostym przypadku dane są podane w sposób jawny.
Najciekawsze instrukcja to budowa żądania z wymuszeniem uwierzytelnienia.
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.
Możecie przetestować ten przykład podając dane poprawne, a potem zmienić nazwę klienta lub hasło.
Efekty widać poniżej:


Dodam tylko, że metod i możliwości obsługi klienta HTTP z poziomu języka ABL jest znacznie więcej. Są np. metody do obsługi nagłówka czy ciasteczek. Może jeszcze o tym napiszę.

Klient HTTP ABL

W całym cyklu artykułów kilkakrotnie pisałem o tworzeniu serwisów generujących dane do tzw. endpoint a także o korzystaniu z tych danych do generowania prostego interfejsu użytkownika.

Żeby nie być gołosłownym przypomnę, że dane takie można wygenerować np. do formatu JSON za pomocą wizardów w OE Develeopr Studio dla serwisów rest
1) poprzez warstwę transportową REST
2) lub warstwę transportową WEB.

Wybierając jedną z powyższych metod tworzymy nowy projekt dla wybranej warstwy transportowej, generujący dane do pliku .cls (tutaj plik customer.cls).

Klasa ta ma metody do manipulacji dla danych, które odpowiadają czasownikom HTTP (Read -> GET, Update -> PUT, Create -> POST, Delete -> DELETE).

Dane dostarczane przez serwis możemy testować przy pomocy np. darmowych rozwiązań jak Chrome Advanced REST client czy Postman. Z pierwszego rozwiązania korzystałem już wcześniej tutaj, a z Postmana w niniejszym artykule. Jest to bardzo intuicyjne, czytelne narzędzie zawierające wszystko co możemy potrzebować przy testowaniu żądań HTTP (patrz poniżej).

Jeśli chodzi o drugą stronę, czyli o klienta czytającego wystawione dane, możemy skorzystać z różnych technologii dostępnych na rynku. Ja pokazałem przykład ze zwykłym JavaScriptem tutaj.

W OE 12 możemy zbudować w programie własnego klienta HTTP, czym zajmiemy się teraz.
W tym celu będziemy korzystać z biblioteki OpenEdge.Net, która udostępnia klasy i interfejsy umożliwiające tworzenie żądań HTTP oraz przetwarzanie odpowiedzi HTTP w języku ABL.

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE oCredentials AS Credentials NO-UNDO.

// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=3000").

// Build the request
oRequest = RequestBuilder:GET(oURI):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN
    MESSAGE "Request Error " + STRING(OResponse:StatusCode) VIEW-AS ALERT-BOX.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, true).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

Kod procedury jest dość prosty. Najpierw budowany jest klient HTTP, potem żądanie, a na koniec żądanie jest wykonywane. Musimy podać tu tylko URI do zdefiniowanego wcześniej serwisu. Instrukcja MESSAGE zwraca nam pobrany JSON, zapamiętajmy nazwy obiektów.

Jeśli chcemy korzystać z poszczególnych danych w programie (no bo po co czytaliśmy JSON?), to wygodnie wczytać je do tabeli tymczasowej w ProDataSecie. Nazwy obiektów muszą być takie jak w JSON, czyli: ttCustomer, dsCustomer. Dodajemy poniższy kod i testujemy.

    DEFINE VARIABLE lRetOK      AS LOGICAL   NO-UNDO.
    DEFINE VARIABLE hdsCust     AS HANDLE    NO-UNDO.
    
    DEFINE TEMP-TABLE ttCustomer LIKE Customer.
    DEFINE DATASET dsCustomer FOR ttCustomer.
    
    hdsCust = DATASET dsCustomer:HANDLE.
    lRetOK = hdsCust:READ-JSON("LONGCHAR", JsonString, "EMPTY").
    FIND FIRST ttCustomer.
    DISPLAY custnum NAME.


Powrócę wkrótce do klienta HTTP z tematem związanym z uwierzytelnianiem. Zachęcam do testów!

Śledzenie aplikacji ABL

Śledzenie aplikacji ABL to nie jest temat nowy – po raz pierwszy zostało zaimplementowane w wersji 9, a więc jeszcze w starym 4GL.
Śledzenie to nie zostało niestety uwzględnione ani w szkoleniu z podstaw języka, ani ze strojenia aplikacji i może dlatego nie wszystkim deweloperom aplikacji OpenEdge jest znane.
W ostatnich tygodniach klienci zgłaszali mi dwa problemy, przy rozwiązaniu których ta technika okazała się pomocna i dlatego postanowiłem o niej napisać.
Chodzi w niej o zrzut określonych danych do własnego pliku log. Dane te mogą dotyczyć np. szczegółów transakcji, łączności z bazą danych, uruchamiania obiektów dynamicznych itd. Można określić także ilość zrzucanych informacji.

Śledzenie można włączyć na różne sposoby. Jeśli chcemy mieć kompletny zapis działania aplikacji (gdy np. nie wiemy gdzie jest wadliwy fragment kodu) możemy uruchomić sesję z określonymi parametrami np.
prowin -p plik.p -db baza -debugalert -clientlog mylog.txt -logginglevel 3 -logentrytypes 4GLTrace,DynObjects.*
Na uwagę zasługują tutaj następujące parametry:

-clientlog – nazwa naszego pliku log

-logginglevel – poziom zapisu, 3 oznacza Verbose i taką wartość będę tutaj stosował

-logentrytypes – typ zrzucanej informacji.

Przyjrzyjmy się wybranym wartościom trzeciego parametru.

  • 4GLMessages – zapisuje komunikaty VIEW-AS ALERT-BOX – jeśli włączony jest Debug Alert (parametr -debugalert), to zapisywane są także informacje stosu ABL
  • 4GLTrace – chyba najczęściej stosowany typ informacji, rejestruje wywołanie procedur, funkcji, trygerów, metod, konstruktorów, destruktorów, zdarzeń PUBLISH/SUBSTRIBE itp.
  • 4GLTrans – rejestrowanie przetwarzanych transakcji i podtransakcji w procedurach ABL
  • DB.Connects – rejestrowanie przyłączania/odłączania baz danych
  • DynObjects.* – rejestrowanie tworzenia i usuwania dynamicznych obiektów różnego typu (* dla wszystkich)
  • QryInfo – rejestrowanie zapytań (OPEN QUERY oraz FOR EACH)
  • Temp-tables – rejestrowanie informacji o tabelach tymczasowych wykorzystywanych w aplikacji
  • TTStats – rejestrowanie statystyk tabel tymczasowych

Niektóre z tych wybranych wartości można stosować jedynie w sesji klienta inne także w sesji serwera aplikacji. Po kompletny wykaz zapraszam do dokumentacji dla konkretnej wersji.

Jest jeszcze inny sposób na uruchomienie tego procesu, szczególnie przydatny jeśli chcemy badać tylko określony fragment aplikacji, co zdarza się chyba o wiele częściej.

Wykorzystuje się do tego obiekt LOG-MANAGER, co pokażę na poniższych przykładach.

/* trace1.p */
DEFINE BUTTON btnCust LABEL "Show Customer".
DEFINE BUTTON btnExit LABEL "Exit".

LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLMessages'.

PROCEDURE ShowCust:
  FIND FIRST customer.
  MESSAGE name
  VIEW-AS ALERT-BOX.
END PROCEDURE.

SUBSCRIBE TO "FindCustomer" IN THIS-PROCEDURE RUN-PROCEDURE "ShowCust".

FORM btnCust btnExit WITH FRAME x.
ENABLE ALL WITH FRAME x.

ON CHOOSE OF btnCust PUBLISH "FindCustomer".

WAIT-FOR CHOOSE OF btnExit.

LOG-MANAGER:CLOSE-LOG.

Pierwszy przykład. Dane są zrzucane do pliku mylogmanager.txt, poziom = 3, a typ informacji to 4GLTrace oraz 4GLMessages. Przykładowa aplikacja GUI składa się z okna, dwóch przycisków i wewnętrznej procedury. Jest też prosta implementacja PUBLISH/SUBSCRIBE. Uruchamiam procedurę, naciskam przycisk btnCust (pojawia się nazwa pierwszego klienta) i następnie btnExit.
Zobaczmy zawartość pliku mylogmanager.txt (dla większej czytelności usunąłem z każdej linii dane czasowe).

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLMessages
2 4GL 4GLTRACE       SUBSCRIBE FindCustomer "C:\WrkOpenEdge128\workspace\ABLProj\trace1.p" C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [Main Block - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 14]
2 4GL 4GLTRACE       PUBLISH FindCustomer [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLTRACE       RUN ShowCust in C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLMESSAGE     Lift Tours
2 4GL 4GLMESSAGE     ** ABL Debug-Alert Stack Trace **
2 4GL 4GLMESSAGE     --> ShowCust C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 10  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         USER-INTERFACE-TRIGGER C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 19  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 21  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
3 4GL 4GLTRACE       Return from ShowCust [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
3 4GL 4GLTRACE       Return from PUBLISH FindCustomer [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
1 4GL ----------     Log file closed at user's request

Z lewej strony widzimy którego typu wpis w danej linii dotyczy. Można np. prześledzić mechanizm PUBLISH/SUBSCRIBE. Widać też jaka wartość została wyświetlona w komunikacie (“Lift Tours”). Ponieważ parametr sesji DEBUG-ALERT jest ustawiony na ‘yes’ to zostaje wyświetlona również informacja stosu ABL (szczególnie przydatna w przypadku wystąpienia błędów). Mamy także informacje o wywołaniu procedury i trygera.

Weźmy teraz inny przykład. Tym razem będą interesować nas transakcje i sub-transakcje, dlatego jako typ informacji dodajemy 4GLTrans.

/* trace2.p */
LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLTrans'.

CustLab:
REPEAT:
   FIND LAST customer.
   DO TRANSACTION:
      country = "USA". 
   END.    
   UPDATE NAME.
   IF NAME BEGINS "x" THEN UNDO, RETRY.
   ELSE LEAVE.
END.   

LOG-MANAGER:CLOSE-LOG.
QUIT.

Przykład składa się tylko z bloku REPEAT wewnątrz którego jest zdefiniowana transakcja, polegająca na edycji pola Name. Jeśli nazwa będzie zaczynać się na literę “x” transakcja ma być wycofana, w przeciwnym razie zatwierdzona. Wewnątrz bloku transakcyjnego zdefiniowano sub-transakcję, w której ustawiany jest kraj “USA”.
Przetestuję procedurę wstawiając najpierw dwa razy nazwę zaczynającą się od “x”, a następnie od innej, poprawnej litery. Pierwsza i druga transakcja powinna zatem zostać wycofana, a trzecia zatwierdzona.

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLTrans
3 4GL 4GLTRANS       BEGIN TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 7]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       END TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
1 4GL ----------     Log file closed at user's request

Sprawdzamy zapisy w pliku mylogmanager.txt. Widać, że pierwsze dwie transakcje (o numerach 28 i 31) zostały wycofane razem z sub-transakcjami (odpowiednio 29 i 32). Trzecia transakcja (nr 34) i sub-transakcja (nr 35) nie zostały wycofane, a więc są zatwierdzone.
Jak widać jest to bardzo użyteczne narzędzie do badania transakcji podczas runtime’u.
Dodam jeszcze, że numery transakcji maja tutaj wartości nadawane lokalnie dla odczytu pliku log i nie mają nic wspólnego z rzeczywistym id transakcji, które można podejrzeć np. w promonie: R&D ->Status Displays -> Processes/Clients… -> 3. Active Transactions

Kafka i OpenEdge cz. II

W poprzednim artykule pokazałem jak zainstalować i skonfigurować lokalne środowisko Kafki a teraz pokażę jak zintegrować je z OpenEdgem.
Niektórzy z Was, którzy będą potrzebować zintegrować się z istniejącą architekturą Kafki, będą zainteresowani od tego właśnie miejsca.

Najlepiej w tym celu sprawdzić dokumentację OE dla danej wersji np. online. Ważne, że trzeba posiadać wersję OE 12.5 lub wyższą.

Pierwszym krokiem jest pobranie biblioteki Apache Kafka C/C++ dla Windows lub Unixa (link Download package po prawej stronie). Minimalna wymagana wersja to 1.7, ale zaleca się pobranie ostatniej wersji.

Gdy plik mamy już pobrany zmieniamy jego rozszerzenie z nupkg na zip i rozpakowujemy. W przypadku Windows będą nas interesowały pliki z podkatalogu runtimes\win-x64\native\, które umieszczamy w katalogu roboczym naszej aplikacji ABL.

Dodajemy do PROPATH następujace biblioteki:

  • $DLC/tty/netlib/OpenEdge.Net.pl
  • $DLC/tty/messaging/OpenEdge.Messaging.pl

Teraz możemy wziąć się za napisanie procedury w ABL, a raczej za pobranie jej z dokumentacji i małą kastomizację.

/* producer.p */

using OpenEdge.Messaging.*.
using OpenEdge.Messaging.Kafka.*.

block-level on error undo, throw.

var RecordBuilder recordBuilder.
var KafkaProducerBuilder pb.
var IProducer producer.
var IProducerRecord record.
var ISendResponse response.

pb = cast(ProducerBuilder:Create("progress-kafka"), KafkaProducerBuilder).

pb:SetBootstrapServers("172.22.13.101:9092").
pb:SetClientId("test client").
pb:SetBodySerializer(new StringSerializer()).
pb:SetKeySerializer(new StringSerializer()).

pb:SetProducerOption("message.timeout.ms", "10000").

producer = pb:Build().

recordBuilder = producer:RecordBuilder.
recordBuilder:SetTopicName("mytopic").   

Find first customer.
recordBuilder:SetBody(name).

record = recordBuilder:Build().

response = producer:Send(record).

producer:Flush(1000).
    
repeat while not response:Completed:
    pause .1 no-message.
end.

if response:Success then do:
   message "Send successful" view-as alert-box.
end.
else do:
    undo, throw new Progress.Lang.AppError("Failed to send the record: " +
        response:ErrorMessage, 0).
end.
 

catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object producer no-error.
end.

Po pierwsze wstawiam własny adres IP w metodzie SetBootstrapServers i nazwę topicu mytopic.
Potem ustawiam SetBodySerializer() (metoda ustawia proces serializacji dla treści wiadomości) na StringSerializer. Serializator zajmuje się konwersją danych na bajty w celu przesłania do topicu. OpenEdge udostępnia również MemptrSerializer i JsonSerializer, jak również tworzenie własnych niestandardowych serializatorów.
I wreszcie znajduję pierwszy rekord customer i ustawiam jego nazwę (name) jako wartość do przesłania.
Po uruchomieniu procedury sprawdzam co pojawiło się w topicu Kafki. Na czerwono zaznaczyłem wartość przesłanej wartości (Lift Tours).

Teraz pora na odczyt wartości z topicu Kafki w procedurze ABL.

/* consumer.p */

block-level on error undo, throw.
 
using OpenEdge.Messaging.ConsumerBuilder from propath.
using OpenEdge.Messaging.IConsumer from propath.
using OpenEdge.Messaging.IConsumerRecord from propath.
using Progress.Json.ObjectModel.JsonConstruct from propath.
using Progress.Json.ObjectModel.JsonObject from propath.
 
var ConsumerBuilder cb.
var IConsumer consumer.
var IConsumerRecord record.
//var Progress.Lang.Object messageBody.
var char messageBody.
 
cb = ConsumerBuilder:Create("progress-kafka").
 
// Kafka requires at least one bootstrap server host and port.
cb:SetConsumerOption("bootstrap.servers", "172.22.13.101:9092").

// Explicitly disable auto commit so it can be controlled within the application.
cb:SetConsumerOption("enable.auto.commit", "false").

// Identify the consumer group. The consumer group allows multiple clients to
//  coordinate the consumption of multiple topics and partitions
//cb:SetConsumerOption("group.id", "my.consumer.group").
cb:SetConsumerOption("group.id", "test-consumer-group").

// Specify whether the consumer group should automatically be deleted when
//  the consumer is garbage collected.
cb:SetConsumerOption("auto.delete.group", "true").

// Configure the consumer's deserializer in order to convert values from
//  the network messages to string objects.
cb:SetConsumerOption("value.deserializer", "OpenEdge.Messaging.StringDeserializer").

// Set the consumer starting position to the most recent message
cb:SetConsumerOption("auto.offset.reset", "latest").
     
// identify one or more topics to consume
cb:AddSubscription("mytopic").
     
// build the consumer
consumer = cb:Build().
          
// loop forever receiving and processing records.
repeat while true:
    // request a record, waiting up to 1 second for some records to be available
    record = consumer:Poll(1000).
         
    if valid-object(record) then do:
           
        // acknowledge the message so the client can resume where it leaves off
        // the next time it is started
        consumer:CommitOffset(record).

        MESSAGE record:TopicName VIEW-AS ALERT-BOX.   

       messageBody = record:Body:ToString().      
       message messageBody view-as alert-box.
      
    end.
         
end.
     
catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object consumer no-error.
end.

Uruchamiamy procedurę producer.p i consumer.p. Widać, że pierwsza wyświetla komunikat o poprawnym wysłaniu, a druga prawidłową wartość pobraną z kolejki.

Na koniec jeszcze jedna informacja związana z ustawianiem wartości parametrów. Otóż można to zrobić na dwa sposoby: poprzez użycie metod akceptujących wartości silnie typizowane (tzw. strongly-typed) lub używając par nazwa-wartość.
Obie poniższe linie kodu są równoważne:

pb:SetProducerOption("bootstrap.servers", "172.22.13.101:9092").
pb:SetBootstrapServers("172.22.13.101:9092").

Kafka i OpenEdge cz. I

O integracji OpenEdge z Apache Kafka chciałem napisać od dawna. Najpierw musiałem pokonać pewne kłopoty techniczne, tak  aby pokazać pełna ścieżkę od instalacji tejże Kafki do napisania programów w ABL do wysyłania i pobierania danych, ale po kolei. Co to jest Kafka.

Na głównej stronie Apache Kafki możemy przeczytać w tłumaczeniu:

Apache Kafka to rozproszona platforma do strumieniowego przesyłania komunikatów typu open-source, z której korzystają tysiące firm w celu zapewnienia wydajnego przetwarzania danych, analizy przesyłania strumieniowego, integracji danych i aplikacji o znaczeniu krytycznym.

Komunikaty w Kafce pogrupowane są w tzw. topiki (tematy, ang. topic). Nadawca (producer), jak i odbiorca (consumer) powiązani są z jednym lub wieloma topikami.
Rzućmy okiem na uproszczony schemat systemu Kafki.

Kafka zapewnia przesyłanie danych w czasie zbliżonym do rzeczywistego ze źródeł, takich jak bazy danych, aplikacje, czujniki, urządzenia mobilne, usługi w chmurze itd.

Nadawcy i odbiorcy mogą być procesami napisanymi w różnych językach programowania, na różnych systemach operacyjnych; musi istnieć tylko mechanizm aby wpięli się do systemu. Dla klientów ABL również jest taki mechanizm ale o tym napiszę później.

Klientom Progressa ta architektura może przypominać produkty z grupy Sonic, jednakże Kafka jest technologią nieporównywalnie popularniejszą, stosowaną przez wiele wielkich i rozpoznawalnych firm jak Netflix, Twitter, Spotify, Cisco, LinkedIn i dziesiątki innych. Zresztą różnic jest więcej. Kafka jest platformą o wysokiej wydajności i skalowalności, przesyłane komunikaty nie są automatycznie kasowane z kolejki po odczytaniu (mogą być przechowywane bez ograniczenia czasowego) itd…

Komunikaty (nazywane także zdarzeniami lub rekordami) z danego topiku dopisywane są na końcu tzw. partycji. Partycja to uporządkowany rejestr komunikatów, mówiąc niskopoziomowo, jest to plik na dysku brokera, do którego zapisywane są komunikaty. Aby konsument był w stanie odebrać określony komunikat / sekwencję komunikatów – musi znać pozycję ostatnio przeczytanego komunikatu.

OpenEdge udostępnia nowe OpenEdge.Messaging API do wysyłania wiadomości do klastra Kafki poprzez Kafka producer i odbierania wiadomości z tego klastra przez Kafka consumer.

Tyle wstępnych informacji. Chcę Wam pokazać cały proces od zainstalowania Kafki na lokalnej maszynie aby każdy mógł przetestować ten system.

Kafka jest przeznaczona do uruchamiania w systemie Linux i Mac i nie jest przeznaczona do natywnego uruchamiania w systemie Windows. Dlatego w przypadku tej platformy zaleca się stosować technologię Docker lub WSL2.

WSL2 (Windows Subsystem for Linux 2) zapewnia środowisko Linux dla komputera z systemem Windows 10+, które nie wymaga maszyny wirtualnej.

Moja konfiguracja wygląda następująco: Windows 10, zainstalowany WSL2 oraz Windows Terminal, w którym mam dostęp do Linuxa Ubuntu.

Instaluję Apache Kafka korzystając z oryginalnej strony. Polega to (ach ten Linux!) jedynie na pobraniu i rozpakowaniu plików. Najpierw niezbędne jest zainstalowanie infrastruktury do utrzymywania i koordynowania brokerów. Może to być KRaft lub ZooKeeper. Ja wybieram to drugie rozwiązanie.

W Windows Terminal otwieram kilka zakładek dla Ubuntu.

W pierwszej uruchamiam ZooKeepera:

sudo bin/zookeeper-server-start.sh config/zookeeper.properties

W drugiej Kafkę:

sudo bin/kafka-server-start.sh config/server.properties

Jak widać cała operacja jest bardzo prosta. Mogę dalej podążać za instrukcją: dodać topik, okno consumera, nadawać i odbierać komunikaty. Ta testowa konfiguracja odwołuje się do parametru bootstrap-server, który zawiera listę par „hostname:port”, które adresują jednego lub więcej brokerów. W tym prostym przypadku dodanie topicu wygląda następująco:

kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092

Zastopujmy teraz wszystkie procesy Kafki poprzez zamknięcie zakładek.

Ponieważ będę potrzebował mieć dostęp z zewnętrznej aplikacji ABL, więc zamiast wartości localhost muszę wstawić wartość ip “maszyny” z Linuxem. Jeśli ip wynosi np. 172.22.13.101, to tę wartość wstawiam w następujących miejscach.
plik: config/server.properties
listeners=PLAINTEXT://172.22.13.101:9092
jak poniżej:

plik: config/producer.properties
bootstrap-servers=172.22.13.101:9092

plik: config/consumer.properties
bootstrap-servers=172.22.13.101:9092

OK, uruchamiam ponownie procesy ZooKeepera i Kafki. W trzeciej zakładce dodaję topic mytopic,

bin/kafka-topics.sh --create --topic mytopic --bootstrap-server 172.22.13.101:9092

a następnie uruchamiam proces producera:

bin/kafka-console-producer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

a w oknie czwartym consumera:

bin/kafka-console-consumer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

Wysyłam przykładowe wiadomości w oknie producera.

Pojawiają się one natychmiast w oknie consumera.

OK, mamy zatem skonfigurowany prosty system Kafki i w następnym wpisie pokażę jak przyłączyć się do niego z poziomu aplikacji ABL.

Podpisane biblioteki procedur ABL

Dziś napiszę wstęp o zabezpieczeniach r-kodów w tzw. podpisanych bibliotekach archiwum procedur. Temat ten wypłynął podczas kwietniowego spotkania World Tour i byłem poproszone o napisanie trochę więcej informacji niż było to w prezentacji.

Biblioteki archiwum ABL to zbiór r-kodów (rozszerzenie .apl), podobnie jak dobrze znane biblioteki .pl ale obsługują także podpisywanie i weryfikację r-kodów.
Podpisany plik archiwum gwarantuje, że żaden r-kod nie został uszkodzony ani naruszony. Jest to bardzo istotne np. podczas przeprowadzanych zewnętrznych audytów aby mieć pewność, że aplikacje przejdą kontrolę bezpieczeństwa.

Deweloperzy dostają dwa narzędzia uruchamiane z wiersza poleceń PROPACK i PROSIGN. Tworzą one cyfrowo podpisane archiwum oparte na standardzie Java JAR.

Zacznijmy od spakowania dwóch plików procedur findcustomer.r i updatecustomer.r wykorzystując komendę PROPACK. Nasz plik wynikowy bedzie miał nazwę cust.apl. Pozostaje do określenia ważny atrybut sygnatury. Może on mieć wartość open (pliki archiwum nie muszą być podpisane) lub required (podpis jest wymagany). Użyję tutaj domyślnej wartości open. Przykład komendy będzie wyglądał następująco:

propack --create --file=cust.apl --vendor "PSC" --signature=open .

Żeby sprawdzić jakie pliki znajdują się w archiwum uruchamiamy komendę z opcją –list:

propack --list --file=cust.apl

Widać, że w bibliotece znajduje się plik MANIFEST.MF
Zawiera on informacje na temat archiwum, a w przypadku podpisanej biblioteki także sygnatury r-kodów (o czym za chwilę).


Zobaczmy teraz co się stanie jeśli utworzę taką bibliotekę z parametrem signature=required.

Dodaję nazwę nowej biblioteki w Protools do PROPATH i uruchamiam prostą komendę.

Dostajemy błąd ponieważ plik musi być podpisany.

Wróćmy do pliku z sygnaturą typu open i zajmijmy się podpisaniem biblioteki. Przed uzyskaniem certyfikatu cyfrowego należy utworzyć magazyn kluczy do przechowywania certyfikatów własnych i z urzędu certyfikacji (CA). Utworzenie magazynu kluczy powoduje również umieszczenie w magazynie certyfikatu z podpisem własnym (self-signed) i pary kluczy (publiczny-prywatny). Nam dla testów będzie wystarczył certyfikat self-signed.
Użyjemy poniższej komendy pamiętając, że w oknie komend musi to być ciąg znaków bez znaków końca linii.

keytool -genkey 
-dname "CN=Host, OU=Education O=PSC, L=Warsaw, S=Mazovia, C=Poland"  
-alias server 
-keystore myKeystore.jks 
-storepass mykeystorepass1 
-validity 90 
-keyalg RSA 
-keysize 4096

Magazyn klucza jest już wygenerowany (.jks). Zapisujemy wartość aliasu i hasła do magazynu.
Teraz możemy użyć komendy PROSIGN.

prosign --archive cust.apl --signedarchive signedcust.apl 
--alias server --keystore myKeystore.jks --storepass mykeystorepass1


Biblioteka jest już podpisana ale dostaliśmy ostrzeżenie, że certyfikat jest self-signed i w środowisku produkcyjnym musimy zadbać o taki, który jest wydany przez oficjalny urząd (CA).

Zobaczmy teraz zawartość pliku MANIFEST.MF. Zawiera on na końcu funkcje skrótu SHA-256 dla obu r-kodów, gwarantując ich oryginalną zawartość.

Manifest-Version: 1.0
Implementation-Title: cust.apl
Implementation-Vendor: PSC
Implementation-Version: 0.0.0.0
Component-Name: cust.apl
Package-Type: apl
Signature-Policy: open
Validation-Policy: warn
Build-OS: all
Build-Date: 2024-05-10T14:57:39.157+01:00
OpenEdge-Tool: propack v1.00 (MSWin32)
OpenEdge-Version: 12.8
Created-By: 17.0.10 (Oracle Corporation)

Name: findCustomer.r
SHA-256-Digest: 1wpcgS+b6ya5OvqTjUek29hJdnqWNsjdRBJTPzYDCyo=

Name: updateCustomer.r
SHA-256-Digest: fKk6cX6dlJsN8VxgKzdbjM1XaXQRL/N3sdlHySHb0vE=

OpenEdge 12.8

Nareszcie mamy na rynku następną wersję LTS (Long-Term Support) OE 12.8. Poprzednia OE 12.2 pojawiła się 4 lata temu. 12.8 zawiera elementy wprowadzane w kolejnych wersjach Innovation 12.3-12.7 włącznie, co z grubsza opisałem na tym blogu. Teraz zobaczymy co ciekawego jest w samej wersji 12.8.

Zacznijmy od administracji PASOE. W poprzednim artykule opisałem nową komendę OEMANAGER, stosowaną do przeglądu statystyk i podstawowych operacji. Osoby zainteresowane odsyłam bezpośrednio do tego wpisu.

Inną ciekawą i bardzo ważną funkcją jest zapowiadana technologia Dynamic Data Masking (DDM) służąca do dynamicznego zaciemniania lub maskowania wrażliwych danych w bazie przed nieautoryzowanymi użytkownikami. Przykładem może być maskowanie w danych osobowych pola “pesel” lub w danych finansowych pola “wynagrodzenie”.

Technologia ta składa się niejako z dwóch etapów. Pierwszy należy do administratora bazy i polega na włączeniu funkcji poleceniem PROUTIL [baza] -C ENABLEDDM. Komenda ta sprawdza czy baza jest licencjonowana do użycia DDM. Następne polecenie PROUTIL [baza] -C ACTIVATEDDM uaktywnia DDM w bazie (dostępne są także opcje DEACTIVATEDDM i DISABLEDDM).

DDM do kontrolowania uprawnień przyznawanych użytkownikom w zakresie maskowania danych wykorzystuje konfigurację opartą na rolach. Administrator DDM może skonfigurować maskę nad polami tabeli, która ukrywa wrażliwe dane w zestawie wynikowym zapytania, a także utworzyć i przypisać nowe tagi autoryzacyjne do ról zdefiniowanych przez użytkownika.

Drugim etapem jest zatem zdefiniowanie własnych ról oraz znaczników autoryzacji i przypisanie ich do zdefiniowanych ról, przydzieleniu ról uwierzytelnionym użytkownikom, konfiguracja masek i znaczników autoryzacji dla pól tabeli. Etap ten realizujemy wykorzystując interfejs IDataAdminService, który udostępnia zestaw właściwości i metod obsługujących operacje CRUD związane z DDM. Poniżej zamieszczam wzięty z dokumentacji przykład utworzenia własnej roli TestUser.

USING OpenEdge.DataAdmin.*.

VAR DataAdminService oDAS. VAR IRole oRole.
VAR LOGICAL lResult.

ASSIGN oDAS = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = oDAS:NewRole("TestUser"). 
oRole:Description = "A Test User".
// This role will be used for DDM
oRole:IsDDM = true.
lResult = oDAS:CreateRole(oRole). 

DELETE OBJECT oDAS.

W OpenEdge jest systematycznie rozwijana technologia wizualizacji danych poprzez standard OpenTelemetry i wykorzystanie zewnętrznych aplikacji typu APM (Application Performance Monitoring).
W 12.8 dodano możliwość monitorowania procesów agentów PASOE oraz klientów ABL. W tym drugim przypadku należy uruchomić sesję ABL z parametrem -otelconfig myotelConfig.json, wskazującym na plik konfiguracyjny w standardzie JSON. Przykładowy taki plik zamieszczam poniżej:

{
"OpenTelemetryConfiguration": {
        "exporters": {
            "otlp": {
                "grpc": [
                    {
                        "endpoint": "http://localhost:4317",
                        "span_processor": "batch",              
                     }
            ]
        }
      }
    },
    "OpenEdgeTelemetryConfiguration": {
        "trace_procedures": "Customer*.p",
		"trace_classes": "*",
        "trace_abl_transactions": true,
        "trace_requires_parent": true,
        "trace_request_start": true
    }
}

Plik ten określa m.in. proces OTel Collectora (port:4317), a także maskę monitorowanych plików np. każda procedura zawierająca w nazwie słowo Customer i wszystkie klasy (*). O pełnej konfiguracji monitorowania przy pomocy Open Telemetry napiszę innym razem.

A teraz kilka słów na temat administrowania bazą. Pojawiła się komenda PROUTIL [baza] -C PROBE [state] która sprawdza aktualny operacyjny stan bazy. Sprawdzane są trzy stany (parametr state):

  • startup – czy baza danych zakończyła uruchamianie
  • liveness – czy broker bazy danych działa i odpowiada
  • readiness – czy baza danych może wykonywać operacje (np. CRUD)

Jeśli wykonanie komendy z parametrem –verbose nie wyświetli żadnych informacji, będzie to oznaczać sukces.

Administratorzy wykorzystujący możliwość zwiększania wartości niektórych parametrów poleceniem PROUTIL… INCREASETO mogą teraz wygenerować plik .pf z aktualnymi ustawieniami podczas runtime’u. Daje to możliwość zapamiętywania i porównywania różnych konfiguracji. Plik możemy wygenerować wykorzystując tablice VST lub z poziomu narzędzia PROMON -> R&D Advanced Options -> 4. Administrative Functions -> 17. Generate parameter file

Dużym zainteresowaniem cieszyła się nowa komenda PROUTIL… TABLEREORG wprowadzona w OE 12.7, która pozwala na przeładowanie tablicy i polepszenie stopnia fragmentacji i rozrzucenia rekordów przy pracującej bazie. Podczas ładowania wykorzystuje się sortowanie rekordów wg indeksu. W OE 12.7 musiał to być indeks unikalny, jednakże czasami lepszą wydajność można uzyskać dla indeksu nieunikalnego ale często używanego w aplikacji. W OE 12.8 indeksy mogą już być nieunikalne.

Inne ciekawe i ważne nowości to np. podniesie wersji Spring Security 6.1.4 oraz Tomcata 10.1.15. Ma to swoje implikacje podczas migracji z wcześniejszych wersji PASOE.
W Developer’s Studio dodano typ projektu .NET z narzędziami: Class Browser, Content Assist, Outline View, co ma pomóc w programowaniu przy użyciu .NET 6.

Jest jeszcze sporo innych nowinek po które zapraszam do dokumentacji.

Strukturalna obsługa błędów w ABL cz. II

W poprzednim odcinku obiecałem napisać o tym jak utworzyć własną klasę obsługi błędów oraz pokazać, że strukturalną obsługę błędów można stosować także w przypadku procedur uruchamianych na serwerze aplikacji. Obie te kwestie pokażę na jednym przykładzie.

W Developer’s Studio klasę definiujemy najwygodniej przy pomocy wizarda, tak jak każda inną klasę. Musimy pamiętać, że dziedziczy ona z klasy Progress.Lang.AppError oraz że trzeba zdefiniować ją jako Serializable, co oznacza, że instancja klasy może być przekazywane przez wartość pomiędzy sesjami AVM (ABL Virtual Machine).

Poniżej widzimy gotową klasę Errors.myError zawierającą właściwości: Errormessage, ErrorNum oraz ProcName oraz dwie metody GetErrorMessage() i GetVerboseErrorMessage(). Posłużą one do własnej obsługi błędów.
Pierwsza metoda zwraca jedynie komunikat wraz z numerem błędu. GetVerboseErrorMessage() zawiera także nazwę procedury, w której ten błąd wystąpił. Gdybyśmy tworzyli program złożony z obiektów moglibyśmy użyć nazwy klasy i metody aby mieć więcej informacji o lokalizacji wystąpienia błędu itp.

USING Progress.Lang.*.
USING Progress.Lang.AppError.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS Errors.myError INHERITS AppError SERIALIZABLE: 

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

    DEFINE PUBLIC PROPERTY ErrorNum AS INTEGER NO-UNDO 
    GET.
    SET. 
    
    DEFINE PUBLIC PROPERTY ProcName AS CHARACTER NO-UNDO 
    GET.
    SET. 

    CONSTRUCTOR PUBLIC myError (
        INPUT pErrorNum AS INTEGER,
        INPUT pErrorMessage AS CHARACTER,
        INPUT pProcName AS CHARACTER):            
        SUPER ().
        ErrorNum = pErrorNum.            
        ErrorMessage = pErrorMessage.
        ProcName = pProcName.
    END CONSTRUCTOR.

    METHOD PUBLIC CHARACTER GetErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum).
        RETURN res.
    END METHOD.

    METHOD PUBLIC CHARACTER GetVerboseErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum) + ". Proc: " + ProcName.
        RETURN res.
    END METHOD.    
    
END CLASS.  

Poniższy przykład jest analogiczny do tego z poprzedniego odcinka – został zmodyfikowany do uruchomienia na serwerze aplikacji, oczywiście z własną obsługą błędów.

USING Errors.myError.

VAR INT i = 1.
DEFINE VARIABLE hServer AS HANDLE NO-UNDO.
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.

CREATE SERVER hServer.
lReturn = hServer:CONNECT("-URL http://localhost:8810/apsv
  -sessionModel Session-managed"). 

IF NOT lReturn THEN DO:
  DELETE OBJECT hServer NO-ERROR.
  RETURN ERROR "Failed to connect to the ABL web application: " + RETURN-VALUE.
END.

RUN catcherror-pasoe-myerror.p ON hServer (INPUT 3000).

CATCH myerr AS MyError:
    MESSAGE myerr:GetVerboseErrorMessage()
        VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
/************************************************************/

/* catcherror-pasoe-myerror.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

USING Errors.myError.

DEFINE INPUT PARAMETER icustNum AS integer.
DEFINE VARIABLE ProcName AS CHARACTER.


FIND customer WHERE customer.custnum = icustNum.
if country NE "Poland" THEN DO:
    ProcName = ENTRY(1, PROGRAM-NAME(1),' ').
    UNDO, THROW NEW myError(555, "Bad country", ProcName).

END. 

Ponieważ użyłem metody GetVerboseErrorMessage() otrzymuję cały komunikat z nazwą procedury w której wystąpił błąd.

Strukturalna obsługa błędów w ABL cz. I

Zanim przejdziemy do omówienia strukturalnej obsługi błędów w języku ABL, trzeba przypomnieć pewne (oczywiste) fakty dotyczące tego języka.

ABL (wcześniej 4GL) był zawsze proceduralnym językiem bazodanowym o strukturze blokowej. Umożliwia on łączenie logiki biznesowej i interfejsu użytkownika w jednym pliku procedury zawierającym instrukcje, blok główny i bloki zagnieżdżone. Blokowa struktura jest tu niezwykle istotna, ponieważ z nią związane są zakresy transakcji, blokady rekordów a także obsługa błędów.

Od samego początku obsługa błędów (nazywana tradycyjną) była zaimplementowana w języku 4GL. Związane są z nią takie słowa kluczowe jak NO-ERROR, STATUS-ERROR, RETURN ERROR, bloki typu ON ERROR itd. Programiści dobrze wiedzą o co chodzi.

W wersji OpenEdge 10 język 4GL wyewoluował do ABL, zawierając oprócz poprzednich instrukcji także technologię programowania obiektowego i razem z tymi zmianami pojawiła się strukturalna obsługa błędów. Jest to model obsługi błędów spotykany w wielu językach, ale zazwyczaj jest kojarzony z językami obiektowymi i składnią typu: try i catch.

Instrukcja try umożliwia zdefiniowanie bloku kodu, z którym można powiązać zachowanie związane z obsługą błędów. Z kolei instrukcja catch służy do zdefiniowania obsługi błędu i powiązanie jej z typem błędu. Gdy system wykryje błąd w bloku try, wykona kod w bloku catch, który pasuje do błędu. Jeśli takiego bloku nie ma, to obsługa błędu jest przekazywana najczęściej w dół stosu wywołań, dopóki system nie znajdzie odpowiedniego bloku catch.

Strukturalna obsługa błędów posiada następujące cechy:

  • Wszystkie błędy są reprezentowane w postaci obiektów
  • Można zdefiniować własne typy błędów (klasy)
  • Można wywołać (throw) błąd explicite
  • Można obsłużyć (catch) określone typów błędów w określonym bloku ABL.
  • Możliwość propagacji błędu (ponowny throw).

Jeśli chodzi o implementację tej technologii w języku ABL, to należy przypomnieć, że jest to język o strukturze blokowej, gdzie prawie wszystkie bloki są blokami transakcyjnymi UNDO (poza zwykłym blokiem grupującym DO:…END.) i nie ma potrzeby budowania specjalnych bloków try, co ułatwia programowanie.

OK, napiszmy pierwszy prosty przykład własnej obsługi błędu, korzystając z faktu, że w tablicy Customer nie ma rekordu o polu CustNum = 1000.

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000.
    MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
    END CATCH.
END.

Wykorzystaliśmy tutaj blok transakcyjny DO ON ERROR UNDO,… W przypadku gdy rekord nie będzie odnaleziony zostanie “wyłapany” błąd systemowy i obsłużony przez nas w bloku CATCH:…END. Blok ten musi być na końcu bloku transakcyjnego. Taki “złapany” błąd przestaje istnieć w sesji ale co jeśli chcemy go przekazać po obsłużeniu do zewnętrznego bloku (np. bloku procedury)? Wystarczy następująca modyfikacja:

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000.
    MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
        UNDO, THROW mySysError.
    END CATCH.
END.

CATCH mySysError AS Progress.Lang.SysError:
    MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX.
END CATCH.

Teraz możemy wychwycić go i obsłużyć ponownie.
Należy dodać, że jeśli użyjemy tradycyjnej składni obsługi błędu NO-ERROR, to ma ona pierwszeństwo nad obsługą strukturalną. W poniższym przykładzie blok CATCH nie będzie więc wywołany.

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000 NO-ERROR.
    IF AVAILABLE (Customer) THEN 
       MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
        UNDO, THROW mySysError.
    END CATCH. 
END.

Poniżej przedstawiłem uproszczony schemat dziedziczenia klas. Progress.Lang.Error służy do wychwycenia dowolnego błędu. Progress.Lang.ProError służy do wychwycenia dowolnego rodzaju błędu Progress: systemu, aplikacji, SOAP. Progress.Lang.SysError służy do wychwycenia błędów języka ABL, np.: niepowodzenie instrukcji FIND, błąd indeksu; wszystko, co obsługuje NO-ERROR. Progress.Lang.AppError służy do wychwycenia błędów aplikacji zgłaszanych (throw) zgodnie z regułami biznesowymi ABL. Z tej klasy może dziedziczyć nasza własna klasa do obsługi błędów.

Zobaczmy teraz jak wychwytywać błędy w procedurach i przesyłać stosowne komunikaty do programu głównego.
W tradycyjnej obsłudze błędów mogliśmy skorzystać ze składni w programie wywołującym: IF ERROR-STATUS:ERROR THEN MESSAGE RETURN-VALUE VIEW-AS ALERT-BOX ERROR.
oraz w wywoływanym RETURN ERROR “Brak rekordu w bazie”.

W obsłudze strukturalnej mamy szersze pole do popisu. Przyjrzyjmy się poniższemu przykładowi.

/* main.p */
VAR INT i = 1.

RUN FindCustomer.p (1000).

CATCH syserr AS Progress.Lang.SysError:
    MESSAGE "Customer: " syserr:GetMessage(1)
       VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
  
CATCH apperr AS Progress.Lang.AppError:
  DO WHILE i <= apperr:NumMessages:
      MESSAGE "Country: " apperr:GetMessage(i)  
                          apperr:GetMessageNum(i)
         VIEW-AS ALERT-BOX INFO BUTTONS OK.
      i = i + 1.    
  END.      
END.

/************************************************************/

/* FindCustomer.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

DEFINE INPUT PARAMETER icustNum AS integer.

FIND customer WHERE customer.custnum = icustNum.
IF country NE "Poland" THEN
    UNDO, THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555).
    
/* Można dodac więcej komunikatów własnej obsługi błędów */    
CATCH e AS Progress.Lang.AppError :
    e:AddMessage("Klient nie jest z Polski", 777).
    undo, throw e.     
END CATCH.

Na początku wywoływanej procedury FindCustomer.p znajduje się instrukcja BLOCK-LEVEL ON ERROR UNDO, THROW. która gwarantuje, że wszystkie nieobsługiwane błędy w blokach transakcyjnych, zostaną propagowane do obiektu wywołującego. Jeśli wywołamy tę procedurę z parametrem 1000 (a więc będzie brak rekordu Customer) błąd zostanie obsłużony w procedurze main.p w bloku CATCH syserr AS Progress.Lang.SysError (ponieważ będzie to błąd systemowy).
Załóżmy, że jeśli rekord zostanie znaleziony, będziemy chcieli sprawdzić czy na pewno klient jest z Polski a jeśli nie, to ustawić własny błąd. Definiujemy więc błąd typu AppError: …THROW NEW Progress.Lang.AppError(“Niewłaściwy kraj”, 555).
Po pewnym czasie chcielibyśmy dodać jeszcze jeden komunikat dla tego błędu. Możemy to zrobić bez problemu gdyż ta klasa posiada metodę AddMessage jak widać poniżej w Class Browser w Developer’s Studio (dla klasy SysError nie ma takiej możliwości).

Możliwości obsługi błędów w aplikacjach ABL jest więc całkiem sporo. Wiem, że niektórzy chcieliby wiedzieć jak poradzić sobie z serwerem aplikacji i czy można pisać własne klasy obsługi, ale o tym następnym razem.

1 2 3 6