OpenEdge 13

10 lutego 2026 roku pojawiła się pierwsza wersja OpenEdge 13, jako tzw, Innovation Release, co stanowi przygotowanie pod wersję długoterminową (LTS). Platforma rozwijana przez Progress wchodzi na kolejny poziom nowoczesności. To wydanie skupia się przede wszystkim na zwiększeniu wydajności, bezpieczeństwa oraz lepszej integracji z nowoczesnymi technologiami w chmurze i kontenerowymi. Dla firm korzystających z OpenEdge oznacza to większą elastyczność, łatwiejsze skalowanie aplikacji oraz lepsze przygotowanie na przyszłe wyzwania technologiczne.

OpenEdge 13 to wydanie zaprojektowane z myślą o nowoczesnych wymaganiach biznesowych i technologicznych. Twórcy platformy położyli szczególny nacisk na bezpieczeństwo, dostępność, wydajność aplikacji oraz coraz ważniejsze obszary, takie jak sztuczna inteligencja i zaawansowane zarządzanie danymi.

Nowa wersja przynosi szereg usprawnień, które mogą realnie wpłynąć na funkcjonowanie systemów w organizacji – niezależnie od jej wielkości. OpenEdge konsekwentnie rozwija się jako platforma wspierająca modernizację istniejących aplikacji, jednocześnie przygotowując je na integrację z nowymi nowoczesnymi technologiami.

W praktyce OpenEdge 13 wprowadza zmiany, które przekładają się na codzienną pracę zespołów IT. Platforma zwiększa produktywność w całym swoim ekosystemie, usprawniając zarówno proces developmentu, jak i zarządzanie środowiskiem. Jednocześnie oferuje lepszą, zoptymalizowaną wydajność – zarówno na poziomie aplikacji, jak i operacji bazodanowych.

Istotnym kierunkiem rozwoju jest również otwarcie na rozwiązania oparte o sztuczną inteligencję. OpenEdge 13 tworzy fundamenty, które umożliwiają integrację aplikacji z ekosystemem AI, co staje się coraz ważniejsze w kontekście analityki i automatyzacji procesów biznesowych.

Nie bez znaczenia są także usprawnienia w obszarze bazy danych. Nowa wersja poprawia przepustowość oraz skraca czas działania narzędzi administracyjnych, co przekłada się na zmniejszenie czasu potrzebnego na konserwację systemu. Równocześnie wzmacnia fundamenty bezpieczeństwa, zapewniając solidne i zgodne ze standardami środowisko pracy.

Niektóre z wprowadzonych nowości możecie znać z wersji 12.8 gdzie były testowane.
Zacznijmy od wybranych nowości w administrowaniu bazy danych.

Wielowątkowe tworzenie kopii zapasowych

Skrócenie czasu wykonywania kopii zapasowych ma bezpośredni wpływ na ograniczenie okien serwisowych i zwiększenie dostępności systemu. W OpenEdge 13 wprowadzono mechanizm wielowątkowego backupu, który znacząco przyspiesza operacje tworzenia kopii zapasowych baz danych w trybie offline.

Tryb wielowątkowy został ustawiony jako domyślny zarówno dla backupów offline, jak i online. Dzięki temu system automatycznie wykorzystuje dostępne zasoby CPU, co przekłada się na krótszy czas realizacji operacji. W razie potrzeby zachowanie to można skonfigurować lub całkowicie wyłączyć za pomocą narzędzia PROBKUP.

probkup [baza] … [ -thread 0 | 1 ] [ -threadnum num-threads ]

Jeśli przed wykonaniem komendy pojawia sie ostrzeżenie ESAM to wynika to stąd, że po wyinstalowaniu poprzedniej wersji nie usunąłem katalogów ESAM.

Wielowątkowe sprawdzanie indeksów (offline)

Sprawdzanie indeksów to kluczowe narzędzie administracyjne wykorzystywane do oceny stanu bazy danych, identyfikacji problemów oraz zapewnienia integralności danych. Odgrywa również istotną rolę w planowaniu i realizacji strategii odzyskiwania po awariach.

W OpenEdge 13 rozszerzono to mechanizm o możliwość wielowątkowego sprawdzania indeksów w trybie offline. Administratorzy mogą teraz równolegle walidować rekordy dla poszczególnych kluczy oraz ich kolejność, co znacząco przyspiesza cały proces.

Zarówno w trybie online, jak i offline, operacje realizowane w ramach opcji 2, 3 i 4 wykorzystują domyślnie przetwarzanie wielowątkowe — pod warunkiem, że środowisko dysponuje odpowiednimi zasobami (CPU, pamięć oraz wydajny dostęp do dysku). Dzięki temu możliwe jest równoległe wykonywanie operacji, które wcześniej były realizowane sekwencyjnie.

proutil [baza] -C idxcheck [ -thread 0 | 1 ] [ -threadnum num-threads ]

Wprowadzono ulepszenia polecenia PROUTIL IDXCOMPACT, służącego do kompresji indeksów.
Jednym z nich jest parametr -compactonly — można jej używać do kompresji drzewa indeksu wyłącznie wtedy, gdy indeks jest unikalny. Opcja ta:
– wyłącza skanowanie drzewa w poszukiwaniu symboli zastępczych usuniętych wpisów indeksu, a zamiast tego przeprowadza jego kompresję;
– wpływa na działanie operacji IDXCOMPACT w zależności od tego, czy określono również opcję -unusedblocks
Aby dokładniej zrozumieć działanie operacji warto przeczytać w dokumentacji z jakich faz składa sie kompaktowanie indexów.

Format notacji naukowej w ABL
ABL obsługuje obecnie odczyt i zapis liczb w notacji naukowej, wykorzystywanej m.in. w formatach takich jak JSON czy XML. Taki zapis umożliwia reprezentowanie bardzo małych lub bardzo dużych wartości za pomocą notacji naukowej.

Dzięki tej funkcji programiści ABL mogą uprościć swój kod i zaoszczędzić czas, eliminując konieczność tworzenia dodatkowych procedur konwersji.

Należy podkreślić, że funkcja ta nie wprowadza nowego typu danych, lecz umożliwia przetwarzanie liczb zapisanych w notacji naukowej. Po ich odczytaniu wartości są nadal reprezentowane wewnętrznie jako jeden ze standardowych typów danych ABL: INTEGER, INT64 lub DECIMAL. W dokumentacji są podane ciekawe przykłady takich liczb, ale zobaczmy dla przykładu dwie poniższe wartości.

/* Średnica czerwonej krwinki człowieka w [m] */
VAR DECIMAL RedBloodCellDiameter = 7e-6.

/* Średnica wszechświata w [m] */
VAR DECIMAL UniverseDiameter = 8.8e26.

DISPLAY RedBloodCellDiameter FORMAT "9.99999E-99".
DISPLAY UniverseDiameter FORMAT "9.99999E+99".

PAUSE.

Przy wyświetlaniu ważne jest podanie formatu. Na takim zapisie można robić konwersję z wartości znakowych.

Pisałem w zeszłym roku o integracji OE Developr’s Studio z AI Coding Assistant. Wymagało to wykonania kilku czynności. W OpenEdge 13.0 to środowisko zawiera już zintegrowanego Asystenta Kodowania AI obsługiwanego przez ChatGPT.

Inną ważną informacją jest to, że platforma .NET 8 uzyskała certyfikat zgodności z OpenEdge 13.0 zarówno dla aplikacji ABL, jak i .NET Open Client. Certyfikat ten gwarantuje, że aplikacje ABL oraz .NET Open Client po aktualizacji do .NET 8 mogą korzystać z funkcji tej platformy, pozostając jednocześnie w ramach wspieranej wersji środowiska .NET.
.NET 8 jest wersją objętą długoterminowym wsparciem technicznym (LTS), którego zakończenie planowane jest na listopad 2026 roku (od 2023 r.).

Wprowadzono jak zawsze wiele ulepszęń w dziedzinie zabezpieczeń, np. wsparcie dla FIPS 140-3.
FIPS (Federal Information Processing Standards) to zestaw standardów opracowanych przez rząd USA, określających wymagania m.in. w zakresie bezpieczeństwa danych i stosowania algorytmów kryptograficznych.

W kontekście OpenEdge ich znaczenie polega na tym, że zapewniają zgodność z uznanymi normami bezpieczeństwa. Dzięki wsparciu FIPS system może korzystać wyłącznie z certyfikowanych, bezpiecznych mechanizmów kryptograficznych, co jest szczególnie istotne w środowiskach wymagających wysokiego poziomu ochrony danych (np. w instytucjach finansowych lub administracji).

Po więcej szczegółów zapraszam do dokumentacji.

OE MCP Connector i VS Code

W świecie aplikacji biznesowych opartych na technologii OpenEdge coraz częściej pojawia się pytanie, jak skutecznie wykorzystać sztuczną inteligencję w pracy z językiem Advanced Business Language. Choć narzędzia AI na stałe zadomowiły się już w wielu ekosystemach programistycznych, technologia OpenEdge pozostawała na uboczu. Głównym problemem była specyfika ABL, którego popularne modele językowe praktycznie nie znają. Z kolei budowa lub dostrajanie własnych modeli LLM to kosztowny i złożony proces, wymagający dużych zbiorów danych oraz specjalistycznej wiedzy.

W najnowszej wersji OE 12.8 deweloperzy dostają bardzo ciekawe narzędzie wspomagające ich pracę – Progress OpenEdge AI Coding Assistant. Jest ono specjalnie zaprojektowane z myślą o wsparciu programistów języka ABL. Zapewnia ono inteligentne, kontekstowe wsparcie w pisaniu, recenzowaniu i modernizowaniu kodu. Właściwie może powinienem napisać: wspaniałe, cudowne narzędzie… ale ten kto korzysta i testuje sztuczną inteligencję wie że trzeba jej “patrzyć na ręce”, bo tu i tam zdarzają się dziwne błędy. W sumie to chyba dobrze, bo programiści mogą jeszcze spać spokojnie. Zdarza się słyszeć opinię, że AI całkowicie zastąpi deweloperów, ale na to przyjdzie jeszcze trochę poczekać.

Asystent wykorzystuje architekturę Retrieval-Augmented Generation (RAG), która wzbogaca działanie modeli językowych poprzez pobieranie trafnych informacji z zewnętrznych źródeł jeszcze przed wygenerowaniem odpowiedzi. Mogą to być repozytoria kodu, dokumentacja projektowa, wewnętrzne standardy, bazy danych czy grafy wiedzy. Dzięki temu OpenEdge AI Coding Assistant nie udziela ogólnych, oderwanych od rzeczywistości podpowiedzi, lecz opiera swoje sugestie na faktycznym kontekście projektu — istniejącym kodzie, obowiązujących regułach i dokumentacji. Efektem są odpowiedzi bardziej precyzyjne, spójne z praktyką zespołu i możliwe do zweryfikowania.

Wykorzystanie Model Context Protocol (MCP) pozwala na ujednoliconą i skalowalną integrację asystenta z różnymi środowiskami programistycznymi, takimi jak Visual Studio Code, Windsurf czy Cursor. Takie podejście oddziela warstwę inteligencji od konkretnego IDE, co nie tylko upraszcza wdrożenie, ale także zapewnia elastyczność w przyszłości. MCP tworzy stabilne ramy do rozbudowy rozwiązania, umożliwiając wprowadzanie nowych narzędzi i adaptację do zmieniających się środowisk bez konieczności kosztownych zmian architektonicznych.

Choć mówimy o OpenEdge AI Coding Assistant, w praktyce instalujemy komponent o nazwie OE MCP Connector, ponieważ to właśnie on stanowi warstwę integracyjną całego rozwiązania. Sam asystent AI to logika i mechanizmy działające „w tle”, natomiast MCP Connector jest elementem technicznym, który łączy środowisko programistyczne (IDE) z silnikiem AI.

Connector wykorzystuje architekturę Model Context Protocol (MCP) do przekazywania kontekstu — kodu, metadanych, reguł czy informacji projektowych — między IDE a usługą AI. Dzięki temu asystent może działać w różnych narzędziach programistycznych w sposób spójny i standaryzowany.
Żeby używać tego produktu z wybranym IDE (ja pokażę na przykładzie VS CODE) trzeby wykonać sporo czynności, które po kolei pokażę.

Po pierwsze przed instalacja IDE, trzeba zainstalować Pythona w wersji 3.9 lub wyższej. Ważne żeby podczas instalacji zaznaczyć pole wyboru „Dodaj plik python.exe do ścieżki PATH”.
Oprócz tego trzeba posiadać ważną licencję OpenEdge 12.2 lub 12.8 i ważną umowę serwisową.
Po drugie musimy mieć konto w programie Progress Software Customer Validation Program (CVP). Logujemy się swoim Proghress ID i sprawdzamy grupę OpenEdge.
Powinniśmy widzieć zawartość a szczególnie dostęp do przypiętego postu: OpenEdge MCP Connector for ABL
Jeśli nie mamy dostępu to trzeba znaleźć na stronie link żeby poprosić o dostęp. Czasem dostęp dostajemy od razu a czasem trzeba poczekać do kilku dni.

Powyższy post zawiera wiele cennych informacji i, przede wszystkim, pliki do pobrania. Pobieramy plik: progress-openedge-mcp-connector-abl-1.0.0.vsix.
Jest on spakowany i musimy go po pobraniu rozpakować.
W VS Code przechodzimy do widoku Extensions i górnego menu po prawej stronie okienka () wybieramy: Install from VSIX… Znajdujemy nasz plik na dysku i po chwili instalacja jest zakończona. Restartujemy VS Code.

Teraz trzeba wygenerować API Key, żebyśmy mogli uwierzytelnić usługę MCP. Logujemy się do Progress Data Cloud. Cały proces jest dokładnie opisany w dokumentacji.
Po zalogowaniu klikamy na ikonę z ustawieniami (gear icon) i Services. Powinniśmy widzieć poniższy widok. Jeśli nie będzie usługi, musimy skontaktowac się z administratorem strony.

Nastęnie klikamy w prawym górnym rogu na ustawienia konta (ikona z ludzikiem), a następnie na nasz ProgressID.

W tych ustawieniach widzimy, że API Key nie jest jeszcze wygenerowany. Klikamy po prawej stronie na: Manage Your API Key i Generate. Wygenerowany klucz pojawi się i możemy go skopiować, aby użyć w naszym IDE.

W VS Code wchodzę w ustawienia pluginu i wpisuję email, który jest skojarzony z moim kontem ProgressID.

Plugin zawiera cały Help, co trzeba zrobić i w jakiej kolejności. I tak widzimy poniżej, że wciskając Ctrl+Shift+P otworzy się okienko poleceń w którym nalezy wpisać: Progress OpenEdge MCP:Login to MCP.

Wtedy pojawi się jeszcze jedno okienko w którym wklejamy nasz klucz API Key. Jesli połaczenie się powiodło widzimy w prawym dolnym rogu OE MCP Connected.
W poprzednim artykule pisałem o pliku z instrukcjami .github/instructions.md. Wykasowuję jego poprzednia zawartość i wpisuję coś jak poniżej. Oczywiście można wpisac o wiele instrukcji, ale na razie wystarczy tyle:

# Copilot Instructions for OpenEdge ABL

## OpenEdge ABL Coding Standards

- All code, scripts, and examples must use OpenEdge ABL syntax and conventions.
- Always use the MCP server OpenEdge and collection "12.8" as the authoritative source for syntax and code examples.

## AI Agent Guidance

- When generating code or documentation, always use OpenEdge ABL syntax and reference the MCP server OpenEdge, collection "12.8" for syntax, examples and best practices.

Restartuję IDE. Otwieram nowy plik procedury .p. W oknie Copilota wpisuję prompta:
Write a new program in OpenEdge ABL that:
1. Prompts the user for a customer number
2. Finds that customer in the Customer table
3. Calculates the total value of all orders for that customer by summing OrderLine.ExtendedPrice
4. Displays the results.

Po chwili dostaję kod programu z poprawną składnią, który generuje poprawny wynik.

/* customer_total.p
   Prompt for customer number, calculate total order value.
*/

DEF VAR iCustNum AS INTEGER NO-UNDO.
DEF VAR dTotal    AS DECIMAL   NO-UNDO INITIAL 0.

/* ask the user for the customer number */
DISPLAY iCustNum LABEL "Customer number".
UPDATE iCustNum.

/* locate the customer record */
FIND Customer WHERE Customer.CustNum = iCustNum NO-LOCK NO-ERROR.
IF NOT AVAILABLE Customer THEN DO:
    MESSAGE "Customer" iCustNum "not found." VIEW-AS ALERT-BOX.
    RETURN.
END.

/* sum extended prices for all order lines of the customer's orders */
FOR EACH Order OF Customer NO-LOCK:
    FOR EACH OrderLine OF Order NO-LOCK:
        dTotal = dTotal + OrderLine.ExtendedPrice.
    END.
END.

/* display the result */
DISPLAY dTotal LABEL "Total Order Value" FORMAT "->>,>>9.99".

RETURN.

Przykład jest dość prosty ale chodzi tu bardziej o instalację i konfigurację MCP Connectora. Zachęcam, jak zwykle, do testów.

W przeciwieństwie do uniwersalnych asystentów kodowania, które oferują szerokie, lecz często powierzchowne wsparcie, OpenEdge AI Coding Assistant został zaprojektowany z myślą o specyfice ABL i ekosystemu OpenEdge. Uwzględnia charakterystyczne wzorce, praktyki i wymagania biznesowe, dzięki czemu stanowi realne, a nie tylko deklaratywne wsparcie dla zespołów pracujących z tym środowiskiem.

OpenEdge i VS Code 2

Każdy z Was zapewne często korzysta z pomocy ze strony sztucznej inteligencji. Warto więc powracać do tego tematu od czasu do czasu.
W Progress OpenEdge ten temat też jest aktywnie rozwijany, ale zanim pojawi się na tym blogu jakiś wpis na temat produktu OpenEdge MCP zobaczmy co możemy zrobić w omawianym niedawno środowisku VS Code.
Chodzi o agenta AI GitHub Copilot, który jest bardzi przydatny do pisania kodu “w locie”.
Jest to narzędzie wyspecjalizowane (tzw. AI pair programmer), które działa bezpośrednio w edytorze IDE, np. VS Code.
Jego zaletą jest błyskawiczne autouzupełnianie kodu i generowanie całych funkcji na podstawie komentarzy bez opuszczania edytora. Poza tym Copilot „widzi” pliki, nad którymi aktualnie pracujemy, dzięki czemu lepiej dopasowuje sugestie do naszego stylu i struktury projektu.

Do korzystania z Copilota w VS Code wymagane jest konto na GitHub. Jest ono niezbędne do uwierzytelnienia subskrypcji i synchronizacji ustawień. Od grudnia 2024 roku dostępna jest wersja Copilot Free, która pozwala na darmowe korzystanie z narzędzia w VS Code (oczywiście są limity).
Po zainstalowaniu rozszerzenia GitHub Copilot w VS Code, zostaniemy poproszeni o zalogowanie się właśnie przez GitHub. Przy logowaniu widzimy trzy opcje, ponieważ GitHub (należący do Microsoftu) wspiera tzw. logowanie społecznościowe.

Loguję się korzystając z mojego wcześniej utworzonegfo konta.

W oknie chata Copilota podaję przykładową instrukcję:
Write a program in openedge ABL that:
1. Prompts the user for a customer number
2. Finds that customer in the Customer table
3. Calculates the total value of all orders for that customer by summing OrderLine.ExtendedPrice
4. Displays the results.

Instrukcje możemy napisać po polsku ale warto jednak pamiętać, że choć Copilot rozumie polski, to jego główne dane treningowe pochodzą z publicznych repozytoriów, gdzie dominuje język angielski. Dlatego precyzyjne terminy techniczne lub bardziej złożone instrukcje mogą czasem dawać lepsze rezultaty w języku angielskim.
Musimy poczekać kilka minut. Poniżej jest wygenerowana procedura.

/* total_customer_orders.p
   Prompts for a customer number, finds the customer,
   sums OrderLine.ExtendedPrice for that customer's orders,
   and displays the total value.
*/

DEFINE VARIABLE iCustNo AS INTEGER NO-UNDO.
DEFINE VARIABLE decTotal AS DECIMAL NO-UNDO INITIAL 0.
DEFINE VARIABLE lHasLines AS LOGICAL NO-UNDO INITIAL FALSE.

PUT UNFORMATTED "Enter customer number: ".
IMPORT UNFORMATTED iCustNo.

FIND Customer NO-LOCK WHERE Customer.CustNum = iCustNo NO-ERROR.
IF NOT AVAILABLE Customer THEN DO:
    MESSAGE "Customer " iCustNo " not found." VIEW-AS ALERT-BOX.
    RETURN.
END.

FOR EACH Order NO-LOCK WHERE Order.CustNum = iCustNo NO-ERROR:
    FOR EACH OrderLine NO-LOCK WHERE OrderLine.OrderNum = Order.OrderNum NO-ERROR:
        ASSIGN decTotal = decTotal + OrderLine.ExtendedPrice
               lHasLines = TRUE.
    END.
END.

IF NOT lHasLines THEN
    MESSAGE "No orders found for customer " iCustNo VIEW-AS ALERT-BOX.
ELSE
    MESSAGE "Total value for customer " iCustNo ": " decTotal VIEW-AS ALERT-BOX.

RETURN.

Tu widzimy błędy jak np. NO-ERROR w pętli FOR EACH, czy sposób wprowadzenia numeru klienta. Stwórzmy plik z instrukcjami dla Copilota. Powinien on spowodować usunięcie NO-ERROR, zmienić sposób deklaracji zmiennych na nowocześniejszy a także wprowadzić pewne zmiany w formatowaniu.

.github/copilot-instructions.md

# Modern OpenEdge ABL Development Standards

## 1. Language Version
- Target OpenEdge ABL 12 or newer only.
- Do not generate legacy ABL syntax.

## 2. Variables
- Always use `VAR` for local variables.
- Never use `DEFINE VARIABLE`.
- Do not specify `NO-UNDO` (default in modern ABL).
- Use the equal sign (`=`) for initial values (e.g., `VAR INT i = 10`).

## 3. Data Types
- Use full data type names only:
  `CHARACTER`, `INTEGER`, `DECIMAL`, `LOGICAL`.
- Do not use abbreviated types.

## 4. Error Handling
- Every procedure or class must start with:
  `BLOCK-LEVEL ON ERROR UNDO, THROW.`

## 5. Formatting
- Use UPPERCASE for all ABL keywords.
- Use 4-space indentation.
- One statement per line.

## 6. Data Retrieval
- Don't use NO-ERROR with FOR loops.

Powtarzamy zlecenie najlepiej dla nowego czata.

/* total_customer_orders2.p
   Prompts for a customer number, finds the customer,
   sums OrderLine.ExtendedPrice for that customer's orders,
   and displays the total value.
*/

BLOCK-LEVEL ON ERROR UNDO, THROW.

VAR INTEGER iCustNo = 0.
VAR DECIMAL decTotal = 0.
VAR LOGICAL lHasLines = FALSE.

PUT UNFORMATTED "Enter customer number: ".
IMPORT UNFORMATTED iCustNo.

FIND Customer NO-LOCK WHERE Customer.CustNum = iCustNo NO-ERROR.
IF NOT AVAILABLE Customer THEN DO:
	MESSAGE "Customer " iCustNo " not found." VIEW-AS ALERT-BOX.
	RETURN.
END.

FOR EACH Order NO-LOCK WHERE Order.CustNum = iCustNo:
	FOR EACH OrderLine NO-LOCK WHERE OrderLine.OrderNum = Order.OrderNum:
		ASSIGN decTotal = decTotal + OrderLine.ExtendedPrice.
		ASSIGN lHasLines = TRUE.
	END.
END.

IF NOT lHasLines THEN
	MESSAGE "No orders found for customer " iCustNo VIEW-AS ALERT-BOX.
ELSE
	MESSAGE "Total value for customer " iCustNo ": " decTotal VIEW-AS ALERT-BOX.

RETURN.

Chciałbym żeby relacje były w skróconej formie ze słowem kluczowym OF, więc dodaję instrukcję na końcu pliku .github/copilot-instructions.md
– Establish relationships between two tables using the OF keyword

OK, trzecia próba wygenerowania procedury.

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

VAR CHARACTER custNum = "".
VAR DECIMAL totalValue = 0.

DISPLAY "Enter Customer Number:".
UPDATE custNum.

FIND Customer WHERE Customer.CustNum = custNum NO-LOCK.

IF AVAILABLE Customer THEN
DO:
    FOR EACH Order OF Customer NO-LOCK:
        FOR EACH OrderLine OF Order NO-LOCK:
            totalValue = totalValue + OrderLine.ExtendedPrice.
        END.
    END.

    DISPLAY
        "Customer Number:" Customer.CustNum
        "Customer Name:" Customer.Name
        "Total Order Value:" totalValue.
END.
ELSE
DO:
    DISPLAY "Customer not found.".
END.

Chociaż nie instruowałem agenta o sposobie wprowadzenia numeru klienta, zmienił on sposób, ale niestety na błędny. Pole custnum nie może występować zanim rekord nie będzie w buforze, jednakże relacje sa napisane prawidłowo. Inny jest sposób podania wyniku co nie powinno nas dziwić, w końcu to sztuczna inteligencja.
No dobrze, ale co dostaniemy jeśli to zadanie skieruje do czataGPT. Nie zna on struktury bazy więc trzeba napisać, że chodzi o bazę testową sports2000.

/*---------------------------------------------------------
   Program: customerOrderTotal.p
   Purpose: Display total order value for a customer
---------------------------------------------------------*/

DEFINE VARIABLE iCustNum    AS INTEGER   NO-UNDO.
DEFINE VARIABLE dTotalValue AS DECIMAL   NO-UNDO.

/* Prompt user for customer number */
UPDATE iCustNum
    LABEL "Enter Customer Number"
    WITH FRAME fInput.

/* Find the customer */
FIND Customer
    WHERE Customer.CustNum = iCustNum
    NO-LOCK
    NO-ERROR.

IF NOT AVAILABLE Customer THEN DO:
    MESSAGE "Customer number" iCustNum "not found."
        VIEW-AS ALERT-BOX ERROR.
    RETURN.
END.

/* Calculate total order value */
FOR EACH Order
    WHERE Order.CustNum = Customer.CustNum
    NO-LOCK,
    EACH OrderLine
    WHERE OrderLine.OrderNum = Order.OrderNum
    NO-LOCK:

    dTotalValue = dTotalValue + OrderLine.ExtendedPrice.
END.

/* Display result */
MESSAGE
    "Customer:" Customer.Name SKIP
    "Total Order Value:" dTotalValue
    VIEW-AS ALERT-BOX INFORMATION.

Nie jest żle bo program jest kompilowalny. Można kazać mu stosować nowy sposób deklaracji zmiennych i skrócony zapis relacji między tablicami i wtedy będzie jeszcze lepiej. Chodzi tu jednak o dobrze znana bazę. Co jeśli mamy własne bazy i bardziej skomplikowany problem? No i co na to Progress? Czy nie ma dla nas jakiegoś bardziej eleganckiego rozwiązania? Prędzej czy później o tym napiszę.

OpenEdge i VS Code

Visual Studio Code (VS Code) jest alternatywą dla tradycyjnego środowiska Progress Developer Studio (PDSOE). Dzięki oficjalnemu wsparciu Progress Software, programiści OpenEdge mogą korzystać z lekkiego i nowoczesnego środowiska IDE.
Konfiguracja VS Code opiera się na pliku konfiguracyjnym w formacie JSON – w głównym folderze projektu musi znajdować się plik openedge-project.json, zawierający definicje OpenEdge jak: wersja OE, ścieżki (PROPATH), połączenia z bazami danych oraz parametry startowe sesji.
Porównując oba środowisku, VS Code jest lżejszy. PDSOE zużywa dużo więcej pamięci i jest wolniejszy. Jednakże za tym drugim środowiskiem przemawia środowisko graficzne (Visual Designer) niezbędne przy tworzenie projektów typu GUI i .NET.
Jeśli chodzi o integrację ze sztuczną inteligencją, to PDSOE ma ograniczone wsparcie w postaci instalacji pluginu AI. VS Code jest silnie zintegrowany z Github Copilot.
Myślę, że niedaleka przyszłość pokaże jak będa się rozwijać oba środowiska.
Zobaczmy teraz jak skonfigurować VS Code żebyśmy mogli tworzyć programy w ABL na przykładzie wspieranego przez Progress rozszerzenia firmy Riverside Software.
Wymaga ono przynajmniej jednej instalacji OpenEdge z licencją deweloperską i jest obecnie testowane z wersjami 11.7, 12.2 i 12.8, choć nie wyklucza się, że może pracować także ze starszymi wersjami.

Po zainstalowaniu pluginu wchodzimy do ustawień klikając ikonę z kołem zębatym (tzw. gear icon). i klikając Settings.

Wybieram z listy zainstalowaną wersję OpenEdge i klikam na Edit in settings.json. Dodaję ścieżkę do katalogu DLC. Powinno to wyglądać m.w. tak:

    "abl.configuration.defaultRuntime": "12.8",
    "abl.configuration.runtimes": [
        {
            "name": "12.8",
            "path": "C:\\OpenEdge128"
        }
    ],

W katalogu projektu należy umieścić plik openedge-project.json zawierający ustawienia sesji openedge.
Na początku najłatwiej go utworzyć z poziomu sesji ABL, wykonując procedurę utworzoną przez Carla Verbiesta.
Uruchamiamy zatem sesję mpro, prowin czy PDSOE z podłączoną bazą danych i uruchamiamy powyższą procedurę. Teraz trzeba dopasować ścieżki poprawiając znaki “/” i “\” w zależności od systemu operacyjnego. Ja mam dla Windows.
Po małej edycji mój plik openedge-project.json wygląda tak:

{
  "name": "application",
  "version": "1.0",
  "oeversion": "12.8",
  "graphicalMode": true,
  "charset": "ISO8859-1",
  "extraParameters": "-basekey ini",
  "workingDirectory": "C:\\vsProject",
  "numThreads": 1,
  "buildPath": [
    {
      "type": "propath",
      "path": "."
    },
    {
      "type": "propath",
      "path": "C:\\OpenEdge128\\gui"
    },
    {
      "type": "propath",
      "path": "C:\\OpenEdge128"
    },
    {
      "type": "propath",
      "path": "C:\\OpenEdge128\\bin"
    },
    {
      "type": "propath",
      "path": "C:\\OpenEdge128\\gui"
    },
    {
      "type": "propath",
      "path": "C:\\OPENED~1"
    },
    {
      "type": "propath",
      "path": "C:\\OpenEdge128\\bin"
    }
  ],
  "dbConnections": [
    {
      "name": "t",
      "connect": "-db C:\\WrkOpenEdge128\\db\\t -S 10001",
      "schemaFile": "dump\\t.df",
      "aliases": [
        "dictdb"
      ]
    }
  ]
}

Teraz trzeba utworzyć w projekcie podkatalog dump i umieścić tam plik t.df ze schematem bazy.

Gotowe. Teraz wystarczy stworzyć plik z rozszerzeniem .p i napisać kod procedury. Wybieramy Ctrl + Shift + p i wpisujemy komendę ABL: Run with prowin. Jeśli kod jest poprawny to zostaje wykonany.
Jeśli napiszemy tylko ABL: to zobaczymy całą listę dostępnych komend.
Powodzenia.

PASOE: Server-Side Profiling

Dwa lata temu napisałem trzeci post z serii o narzędziu Profiler – narzędziu deweloperskim wykorzystanym w lokalnych sesji klienckich.
Tym razem napiszę o trudniejszym użyciu tej techniki dla sesji serwera PASOE. Dane profilowania można zbierać zarówno dla modelu production jak i development.

Zbieranie danych z PASOE różni się w porównaniu z lokalną sesją. Pierwszym krokiem jest utworzenie specjalnego magazynu diagnostycznego, do którego będa trafiały dane z jednego lub kilku agentów wielosesyjnych

OpenEdge udostępnia w tym celu aplikację o nazwie oediagstore.war, którą można wdrożyć na instancji PASOE. Ponieważ ilość magazynowanej informacji będzie dla nas prawdopodobnie zbyt duża będziemy mogli ją przefiltrować. Filtry są konfigurowane i włączane za pomocą interfejsów JMX i REST API.
Ostatnim krokiem będzie użycie Profiler Viewer w Developer Studio, aby wyświetlić profilowane dane, jak opisałem to już kiedyś dla lokalnych danych.

Pierwszym krokiem jest utworzenie instancji serwera PASOE demoDiagnosticStore, która będzie zarządzać bazą danych (nie jest to instancja która zarządza naszą aplikacją!):
pasman create -v -p 8850 -P 8851 -s 8852 -f demoDiagnosticStore
i wdrażamy aplikację zarządzająca magazynem danych, która znajduje się w katalogu %DLC%\servers\pasoe\extras\oediagstore.war:
pasman deploy -I demoDiagnosticStore %DLC%\servers\pasoe\extras\oediagstore.war
Teraz w podkatalogu instancji work tworzymy pustą bazę danych oediagdb (jeśli chcemy utworzyć bazę o innej nazwie musimy zmienić to w ustawieniach AppServer.SessMgr w pliku conf/openedge.properties).


Możemy sprawdzić w OE Management czy baza ta będzie uruchamiana przez agentów PASOE:

Teraz uruchamiamy serwer bazy:
proserve oediagdb -S 5555
i uruchamiamy sesję kliencką:
prowin -db oediagdb
W oknie procedur wpisujemy prosty kod do załadowania schematu bazy:

RUN prodict/load_df.p (INPUT "oediagstore.df").
QUIT.

Można zamknąć okno edytora i zrestartować PASOE aby połączył się z bazą.
pasman pasoestart -I demoDiagnosticStore -timeout 200

Do przetestowania profilowania potrzebujemy jeszcze naszą instancję roboczą, tutaj jest to instancja o nazwie profileTargetInstance. Musi mieć ona wdrożoną aplikację oemanager.war.

Będziemy teraz tworzyć (w edytorze tekstowym) i uruchamiać zapytania JMX. Należy dokładnie zwracać uwagę na składnię tych zapytań.
Pierwsze zwraca ID uruchomionych agentów dla tej instancji.

{"O":"PASOE:type=OEManager,name=AgentManager","M":["getAgents","profileTargetInstance"]}

Zapytanie zapisujemy jako agentInfo.qry w podkatalogu /bin dla instancjini i uruchamiamy z wiersza poleceń.
oejmx.bat -R -Q agentInfo.qry
Wynik działania znajduje się w pliku tekstowym wygenerowanym do podkatalogu /temp.

{"getAgents":{"agents":[{"agentId":"nKD8vEGLSju-p2mgwQXrdw","pid":"27564","state":"AVAILABLE"}]}}

Dla nas ważny jest agentID, ktory posłuży do napisania drugiego zapytania, którego zadaniem jest włączenie przechwytywania danych diagnostycznych dla następnych 100 żądań o status serwera. Używa również filtrów AdapterMask i Proclist (u nas są puste).
Uwaga: w zapytaniach JMX nie może być znaków łamania wiersza. Tutaj są tylko dla lepszej czytelności.

{"O":"PASOE:type=OEManager,name=AgentManager",
"M":["pushProfilerData","nKD8vEGLSju-p2mgwQXrdw",
"http://localhost:8850/oediagstore/web/diag",100,
"{\"Coverage\":true,\"AdapterMask\":null,
\"ProcList\":null,\"TestRunDescriptor\":\"Sample Test\"}"]}

Plik zapisujemy jako diagInfo.qry i uruchamiamy:
oejmx.bat -R -Q diagInfo.qry
Efekt widać ponownie w pliku tekstowym w podkatalogu /temp.

{"pushProfilerData":
{"ABLReturnVal":true,
"agentId":"nKD8vEGLSju-p2mgwQXrdw",
"pid":"27564"}}

Jeśli ABLReturnVal wynosi true to znaczy, że uruchomienie zapytania zakończyło się sukcesem.
Piszemy trzecie zapytanie żeby dostać ustawienia profilera i zapisujemy je w pliku profilersettings.qry.

{"O":"PASOE:type=OEManager,name=AgentManager",
"M":["getProfilerSettings","nKD8vEGLSju-p2mgwQXrdw"]}

Uruchamiamy query oejmx.bat -R -Q profilersettings.qry
i otrzymujemy następujący wynik, czyli zestaw ustawień.

{"getProfilerSettings":{"ABLOutput":{"AdapterMask":"APSV,SOAP,REST,WEB",
"ProcList":"",
"RequestHiBound":-1,
"TestRunDescriptor":"Sample Test",
"bufsize":128,
"RequestLoBound":0,
"Coverage":true,
"RequestCount":100,
"Statistics":false,
"URL":"http://localhost:8850/oediagstore/web/diag"},
"ABLReturnVal":true,
"agentId":"nKD8vEGLSju-p2mgwQXrdw",
"pid":"27564"}}

Żeby wymusić jakiś prosty test i wygenerować dane do magazynu diagnostycznego wystarczy zwykła komenda ping:
curl http://host:port/rest/_oepingService/_oeping

Teraz w podkatalogu /work dla instancji demoDiagnosticStore uruchamiamy edytor podłączony do bazy oediag.db:
prowin -db oediag.db i piszemy kod ABL który ma pobrać dane z bazy i utworzyć plik dla profilera  o rozszerzeniu .prof.

DEFINE VAR requestId as Integer NO-UNDO.
DEFINE VAR fileName as Character NO-UNDO.
FOR EACH ProfiledRequest:    
	requestId = requestId + 1.    
	fileName =  "C:/OpenEdge/WRK/profiler_" + STRING(requestId) + ".prof".
    COPY-LOB ProfiledRequest.PerfData TO FILE fileName.
    MESSAGE fileName VIEW-AS ALERT-BOX.
END.

Plik .prof otwieramy w Developers Studio i widzimy znajomy widok.

Jak widać technika ta jest dość żmudna, ale w końcu otrzymujemy dane do analizy.

Dynamic Data Masking II

Kolejnym krokiem jest zdefiniowanie masek dla wybranych pól. Maski te zasłaniają nieautoryzowanym użytkownikom część lub całą zawartość pola. Mamy cztery rodzaje takich masek: domyślna, częściowa (partial), literal, null.

Wszystkie ustawienia DDM w ABL (tak jak w poprzednim artykule) realizujemy poprzez metody DataAdminService. Zaczynamy od maski domyślnej, w ktorej mamy małe pole do menewru. Pola znakowe są oznaczane jako XXX (liczba X zależy od maskowanych danych), pola decimal 0.00 itd. Maska jest ustawiana poprzez prefiks “D:”. Zaczniemy od pola Balance.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Balance","D:","#DDM_SEE_ContactInfo").

Ustawmy taką samą maskę także na pola znakowe SalesRep i State.

Żeby ustawienia maskowania były widoczne trzeba aktywować usługę DDM w bazie poleceniem:
proutil sports2020 -C activateddm.

Do testowania użyjemy prostej procedury findCust.p.

// findCust.p
FIND FIRST customer.
DISPLAY customer EXCEPT comments WITH 1 COLUMN.
PAUSE.

Uruchamiamy dwie sesje klienta z bazą sports2020 logując się raz jako Admin i raz jako User (patrz poprzedni artykuł). Poniżej widać porównanie danych dla obu sesji – różnice zaznaczyłem na czerwono.

Maska literal służy do maskowania danych gdy chcemy zamiast originalnej wartości podać własną. Należy pamiętać, że dla każdego rodzaju maski obowiązuje zasada zgodności typów, i tak tutaj dla pola znakowego City możemy podać własną maskę np. “UKRYTE“, dla pola PostalCode 00000 itd.
Maska literal jest ustawiana poprzez prefiks “L:”.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","City","L:UKRYTE","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","PostalCode","L:00000","#DDM_SEE_ContactInfo").


Pora przejść do najciekawszej maski, czyli częściowej, dającej najwięcej możliwości, ale stosowanej tylko dla pól znakowych.
Składnia maski wygląda tak: P:prefix,char[:maxchars][,suffix]

prefix – określa liczbę niezamaskowanych znaków, które mogą być wyświetlane na początku pola.
char[:maxchars] – określa pojedynczy znak ASCII, który ma być użyty do ukrycia wartości kolumny, maxchars (wartość opcjonalna) określa maksymalną liczbę znaków do zamaskowania przy użyciu podanego znaku ASCII, reszta ciągu jest przycinana.
[,suffix] – (wartość opcjonalna) określa liczbę znaków na końcu pola, które mogą być niezamaskowane.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Phone","P:5,*:2,4","#DDM_SEE_ContactInfo").

Testujemy pole Phone. Działa to w następujący sposób: pierwsze 5 znaków jest niezamaskowana, podobnie jak ostatnie 4 znaki, to co w środku jest zamaskowane maksymalnie 2 znakami “*“. Jeśli długość pola będzie miała tylko 10 znaków to będzie wyświetlana tylko jedna gwiazdka itd.

Ostatnim rodzajem maski jest null, którą można stosować dla każego typu danych. Zamaskowane wartości są wyświetlane jako “?“. Maskę null ustawiamy za pomocą prefiksu N:.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","CreditLimit","N:","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","Terms","N:","#DDM_SEE_ContactInfo").


Jeśli trzeba usunąć maskowanie z pola musimy posłużyć się metodą unsetDDMMask. Usuńmy maskę np. z pola State.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")). 

lResult = service:unsetDDMMask("Customer","State").


Przy pomocy metod DataAdminService można manipulować rolami, tagami autoryzacji itp. Możemy podejrzeć np. tag i maskę dla danego pola.

USING OpenEdge.DataAdmin.*. 
VAR DataAdminService oDAS. 
VAR CHARACTER cMask. 
VAR CHARACTER cAuthTag. 

oDAS = new DataAdminService (ldbname("DICTDB")). 
oDAS:GetFieldDDMConfig ("Customer","Phone", OUTPUT cMask, OUTPUT cAuthTag).
MESSAGE cMask SKIP cAuthTag
VIEW-AS ALERT-BOX. 

Poniżej widać dane dla pola Phone.

OK, na tym na razie kończę. Do tematu powrócę jeśli będą od Was jakieś pytania.

Dynamic Data Masking I

Na początku chciałbym podać nieco wiadomości wprowadzających do Dynamic Data Masking (DDM). Najprościej mówiąc jest to zabezpieczenie danych w bazie na poziomie logicznym, ale definiowane przez administratora.
Wyobrażmy sobie sytuację gdy deweloper tworzy aplikację, w której wyświetlane są dane wraźliwe jak np. numer telefonu, adres lub nawet pesel czy miesięczne zarobki. Z drugiej strony są osoby w firmie, które powinny te dane widzieć. Możemy to rozwiązać na poziomie pisania aplikacji lub zrobić to globalnie na poziomie bazy. Takie rozwiązanie zmusza każdego użytkownika do zalogowania się w bazie i, w zależności od uprawnień, daje wgląd do wybranych pól w tabelach.
Administrator DDM (lub administrator zabezpieczeń) może skonfigurować maskę dla pól tabeli, która ukrywa poufne dane w zestawie wyników zapytania. Kontroluje on również uprawnienia dostępu użytkowników do przeglądania niezamaskowanych wartości określonych pól.
Żeby korzystać z DDM trzeba mieć licencję na dodatek The OpenEdge Advanced Security (Progress OEAS) i serwer bazy danych Enterprise. Minimalna wersja to 12.8.4.

OK, tworzymy testową bazę, kopię bazy sports2020 i włączamy funkcję DDM.
proutil sports2020 -C enableddm
Sprawdzamy czy funkcja ta jest dodana (na razie jeszcze nieaktywna).
proutil sports2020 -C describe

Pierwszym krokiem jest dodanie użytkowników do bazy. W dokumentacji online jest to zrealizowane przez program w obiektowym ABL. Ja zrobię to standardowo w narzędziu Data Administration -> Admin -> Security -> Edit User List.
Dodaję użytkowników: Admin, User. Oczywiście można wykorzystać tutaj konta użytkowników zdefiniowanych nie w bazie ale w zewnętrznych systemach uwierzytelniania.

Loguję się jako Admin i ustawiam go jako security adminisrtator Admin -> Security -> Security Administrators.

Warto zablokować dostęp podczas runtime’u dla niezalogowanych użytkowników oraz ustawić sprawdzanie uprawnień kont także podczas runtime’u.Admin -> Database Options

Aby określić uprawnienia użytkownika do dostępu do zamaskowanych danych, trzeba utworzyć w kodzie ABL rolę dla użytkowników zdefiniowanych w bazie (operacja mapowania użytkownika do roli).
Poniższy przykładowy kod tworzy rolę myRole, którą wykorzystamy do zdefiniowania uprawnień przyznawanych użytkownikom do demaskowania danych:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service. 
VAR IRole oRole.
VAR LOGICAL lResult.

service = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = service:NewRole("myRole"). 
oRole:Description = "Role for DDM Admin".
oRole:IsDDM = true.
lResult = service:CreateRole(oRole). 

DELETE OBJECT service.

Następnym krokiem jest utwórzenie tagu autoryzacji i powiązania go z rolą utworzoną w poprzednim kroku. Tag autoryzacji ustanawia połączenie między zdefiniowanymi przez użytkownika rolami DDM a polami tabeli, do których ma zostać zastosowana maska. Nazwa tagu musi zaczynać się od #DDM_SEE_.

Poniższy kod kojarzy tag autoryzacji #DDM_SEE_ContactInfo z rolą myRole:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service.
VAR IAuthTag oTag.
VAR LOGICAL lReturn.

service = NEW DataAdminService (LDBNAME("DICTDB")).

     oTag = service:NewAuthTag("#DDM_SEE_ContactInfo"). 
     oTag:RoleName = service:GetRole("myRole"):Name.
     oTag:description = "To see contact info".
     lRETURN = service:CreateAuthTag(oTag).

No i wreszcie przydzielamy zdefiniowaną rolę użytkownikowi Admin.

USING OpenEdge.DataAdmin.*.
 
VAR DataAdminService service.
VAR IGrantedRole oRole.
VAR IUser oUser.
VAR LOGICAL lResult = FALSE.
 
service = NEW DataAdminService(LDBNAME("DICTDB")).
oRole = service:NewGrantedRole().
oRole:Role = service:GetRole("myRole").
oUser = service:GetUser("Admin").
 
IF VALID-OBJECT(oRole:Role) AND VALID-OBJECT(oUser) THEN DO:
  oRole:Grantee = oUser:Name.
 
   IF oUser:Domain:Name NE "" THEN
      oRole:Grantee = oRole:Grantee  + "@" + oUser:Domain:Name.
 
    oRole:CanGrant = false. // Cannot grant to others.
    // Granting Role to Admin
   lResult = service:CreateGrantedRole(oRole).
   
END.

Następnym krokiem jest zdefiniowanie maski dla wybranych pól i przetestowanie przykładów, ale tym zajmiemy się w następnym artykule. Teraz chciałbym pokazać gdzie można szukać informacji o zefiniowanych rolach i tagach.
Elementy te zostały dodane do metaschematu bazy, a jeśli tak, to są w określonych nowych tablicach VST.
Po pierwsze możemy zrzucić dane do plików poprzez opcję w Data Administration Admin -> Dump Data and Definitions -> Security Permissions.
Poniżej widać pierwsze linie z dwóch plików, w których są zdefiniowane przez nas dane.

_sec-auth-tag.d

"myRole" "#DDM_SEE_ContactInfo" "Can see contact info"
...

_sec-granted-role.d

"f82629bb-b26d-6b83-d314-c5d870ea3fee" "Admin" "myRole" no "Admin" ""
...

Informacja w plikach pochodzi z poniższych ukrytych tablic VST.

Wykorzystując je możmy sami wygenerować potrzebne informacje jak w poniższym przykładzie.

FIND FIRST _sec-granted-role NO-LOCK.
   DISPLAY  _grantee _role-name  WITH SIDE-LABELS.

FIND FIRST _sec-auth-tag NO-LOCK.
   DISPLAY  _sec-auth-tag._role-name _auth-tag SKIP 
            _description WITH SIDE-LABELS.


OK, następnym razem weźmiemy na warsztat tworzenie różnych masek i testowanie programów.

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!

1 2 3 6