Project

General

Profile

Embedded Mode Developer Guide

This page explains how embedded mode works, end to end, and how to develop, test and debug an embedded application. The reference details (configuration nodes, JSON formats, API signatures, JS module interfaces) are in Embedded Mode Web Client.

Introduction

Embedded mode is a way to run a converted 4GL GUI application inside a modern web application. The legacy screens are rendered by the FWD web client in an <iframe>, and the web application around it (the host page) is ordinary HTML, CSS and JavaScript, written and styled freely by web developers.

The host page usually provides:

  • the navigation: a menu or module tree, a list of the open windows, the tabs (pages) of the current window;
  • the outer look: header, side bars, footer, colors, fonts, all in the application's own style;
  • new web content which does not exist in the legacy application: dashboards, charts, search, favourites, ...;
  • integration between the new web content and the legacy screens, through the up-call APIs (see "Up-calls").

Why it is useful:

  • The application gets a modern web look without rewriting the 4GL screens: every existing window keeps working, unchanged, in the central area.
  • New features can be added gradually, in the host page, by web developers, with any web technology.
  • The new web content can call 4GL business logic, and the 4GL code can send data and notifications to the web content, so both sides work as one application.

Key facts:

  • Embedded mode applies to the Web GUI client only. The Swing GUI client, the ChUI clients and the FWD Virtual Desktop are not embedded.
  • The 4GL windows are drawn without decorations: FWD draws no title bar, no border and no minimize/maximize/close buttons in the iframe. Everything around the 4GL window (title, close button, menus, window list, tabs) is HTML in the host page, designed and styled by the application (see "What the host page draws"). Dialog boxes and other modal windows are still decorated by FWD.
  • Only one 4GL window is shown at a time in the iframe (plus the modal dialogs opened over it).
  • The 4GL widgets are drawn on a canvas by FWD, using the active FWD theme (see Themes). The host page's CSS styles the host page only; it can not change the widgets in the iframe.
  • Embedded mode requires an SSO authenticator (see "Authentication and session creation").
  • All the embedded web resources (FWD's and the application's) are served from the classpath (the jars), so a change to a JS, CSS or HTML file needs a rebuild and a server restart (see "Development Process").

The examples on this page come from the Hotel GUI demo application, which is set up for embedded mode.

How Embedded Mode Works

The three tiers

An embedded application runs in three places:

+--------------------------------------------------------------+
| Host page (browser, outer document)                          |
|   p2j.embedded.js, p2j.emain.js, application JS/CSS          |
|                                                              |
|   +------------------------------------------------------+   |
|   | <iframe>: FWD web client (browser)                   |   |
|   |   canvas with the 4GL window                         |   |
|   +------------------------------------------------------+   |
+-------------------------------|------------------------------+
                                | websocket
+-------------------------------v------------------------------+
| FWD client JVM (one per user session)                        |
|   GuiWebDriver: draws the UI, relays the embedded calls      |
+-------------------------------|------------------------------+
                                | FWD protocol
+-------------------------------v------------------------------+
| FWD server: all converted 4GL code (emain.p,                 |
|   fwd-embedded-driver.p, the application), database          |
+--------------------------------------------------------------+
  • Host page <-> iframe: window.postMessage, with JSON messages ({type, mode, id, payload}). The host page never talks to the websocket directly; p2j.embedded.js (host side) and p2j.remote.js (iframe side) do the relaying.
  • iframe <-> FWD client: the normal web client websocket, with additional message types for embedded mode (invoke, remote call, publish/subscribe, desktop resize, status and message text).
  • FWD client <-> FWD server: the normal FWD protocol, as for any client.

As for any FWD client, all the converted 4GL code runs on the FWD server: emain.p, fwd-embedded-driver.p and the application programs. The FWD client JVM only renders the UI and relays the embedded calls: an invoke* request from the host page is forwarded by the client to the server (GuiWebSocket), and a P2J-REMOTE-CALL executed on the server is sent through the client to the host page.

The two sides of the postMessage channel accept each other only after a handshake. When the FWD client is ready, the FWD page in the iframe generates a random number (the secret) and sends it to the host page in a PAGE_LOADED request. The host page checks that the request comes from its iframe, and answers with the same number. The FWD page accepts the answer only if it comes from the parent window and carries the same secret. After that, both sides only accept messages from the origin they recorded.

Conversion

Embedded mode needs one conversion option, in cfg/p2j.cfg.xml:

<parameter name="add-embedded-windows" value="true" />

With this option, conversion finds the ADM windows, adds to their programs generated 4GL functions which the driver uses at runtime (fwdGetWindow, fwdGetMainFrame, fwdGetPages, ...), and writes the list of these windows in adm_windows.json, in the project's root folder. The functions and the file format are described in FWD conversion augmentation for ADM programs.

adm_windows.json can be edited by hand after conversion: a window which should not be available in embedded mode is just removed from it. Conversion rewrites the file each time it runs, so a window removed by hand must be removed again after each conversion (or filtered out by the host page's populateWindows). Note that ant clean (which also runs as part of ant deploy) deletes it, together with the converted code.

The window title in adm_windows.json is the TITLE assigned to the window in the 4GL code (when it is a constant; otherwise the program name). It is not shown inside the iframe: the host page uses it to name the windows before they are run, e.g. in the window list, and p2j.emain.setTitle shows it as the title of the current window.

Two 4GL programs are part of every embedded application:

Program Owner Purpose
emain.p application The startup procedure of every embedded client. It defines the shared variables fwd-user-logout, fwd-custom-apis and fwd-embedded-mode, implements doLogin (see "Authentication and session creation"), optionally implements the custom APIs (see "Integration Points in the 4GL Code"), and waits with WAIT-FOR CLOSE OF THIS-PROCEDURE.
fwd-embedded-driver.p FWD (src/4gl/embedded/) The driver which runs, shows, switches and closes the ADM windows, and serializes their pages, menu bar and toolbar for the host page. The host page runs it persistently and calls its inMsg_* procedures. It is copied into the application's 4GL sources by the build (Hotel GUI: target init) and converted with the application. Do not modify it.

Configuration

The directory nodes are described in Runtime Configuration. In short:

  • /server/default/webClient/embedded = TRUE enables embedded mode;
  • an SSO authenticator must be configured (ssoplugin in /security/config/auth-mode); without it, embedded mode is not enabled, even when webClient/embedded is TRUE;
  • a remoteentrypoint ACL is needed for every external program run from the host page, at least emain.p and fwd-embedded-driver.p;
  • runtime/default/dynamic-layout/embedded controls the window sizing (see "Sizing and resizing"), enhancedBrowse/embedded the enhanced browse options in embedded mode.

Hotel GUI's SSO configuration:

<node class="authMode" name="auth-mode">
  <node-attribute name="mode" value="4"/>
  <node-attribute name="retries" value="-1"/>
  <node-attribute name="plugin" value="com.goldencode.p2j.security.GuestAccess"/>
  <node-attribute name="ssoplugin" value="com.goldencode.hotel.HotelGuiSsoAuthenticator"/>
  <node-attribute name="ssopluginoption" value="bogus"/>
</node>

The directory is read when the server starts: changes require a server restart.

Packaging

All the embedded content is served by the FWD server, from the classpath: the FWD JS from p2j.jar, everything else from the application jar. The URLs are listed in URLs served for the embedded application.

In Hotel GUI, the build target prepare.embedded (run by jar and by deploy.prepare) copies the application's embedded sources into the application jar:

Source Ends up in hotel.jar as
embedded/src/resources/custom/* (emain.js, ehotel.js, ehotel.css, images, fonts, libraries) com/goldencode/hotel/embedded/*, served as /embedded/custom/*
adm_windows.json (generated by conversion) com/goldencode/hotel/embedded/adm_windows.json
embedded/src/com/goldencode/hotel/* (the SSO authenticator) compiled with the application
webres/* (web_partial_login_embedded.html, ...) com/goldencode/hotel/webres/*
webres/dashboard/* (HTML-BROWSER content) also copied into the jar with the rest of webres/, but served at runtime from the loose copy in deploy/client/webres/dashboard, made by deployapp.prepare (see "Adding web content")

The FWD JS (/embedded/fwd/*) always comes from p2j.jar: the copies which prepare.embedded makes in embedded/src/resources/fwd/ are not used at runtime.

Setup checklist

To set up embedded mode for an application (Hotel GUI is the reference):

  1. Conversion: add-embedded-windows = true in cfg/p2j.cfg.xml (see "Conversion").
  2. 4GL sources: emain.p (application) and fwd-embedded-driver.p (copied from FWD by the build) are in the converted sources, and both are in the root list (Hotel GUI: cfg/rootlist.xml), so they are converted as entry points.
  3. emain.p: defines the shared variables fwd-user-logout, fwd-custom-apis and fwd-embedded-mode, sets fwd-custom-apis = THIS-PROCEDURE (or to the program which implements the custom APIs), implements doLogin and waits with WAIT-FOR CLOSE OF THIS-PROCEDURE.
  4. Directory: webClient/embedded = TRUE (see "Configuration").
  5. SSO authenticator: an implementation of SsoAuthenticator in the application, configured as ssoplugin in /security/config/auth-mode, and an OS user for the FWD account it returns (see "Authentication and session creation").
  6. ACLs: a remoteentrypoint ACL for emain.p, fwd-embedded-driver.p and every other external program run from the host page.
  7. Host page: web_partial_login_embedded.html in webres/, packaged into the application jar (see "Packaging").
  8. Custom emain: emain.js (and the other application JS/CSS) in embedded/src/resources/custom/ (see "The JavaScript Side").

Authentication and session creation

Embedded mode has two logins: the SSO authentication which starts the FWD client, and the 4GL login done by emain.p.

  1. The browser loads /embedded. The server returns the host page (web_partial_login_embedded.html, inlined in FWD's landing page).
  2. The host page loads its scripts (see "Loading the host page") and calls p2j.emain.initialize(customEmain).
  3. The user enters the user name and password; the login button calls p2j.emain.login(user, password), which POSTs usr and psw to /embedded.
  4. The server calls the SSO authenticator's authenticate(...) (an implementation of com.goldencode.p2j.security.SsoAuthenticator). It validates the credentials and returns the FWD account to use. Hotel GUI's HotelGuiSsoAuthenticator checks the user against the application's meta_user table and always runs the client as the FWD account given in ssopluginoption (bogus). The client JVM is spawned under the OS user associated with that FWD account (see "The SSO authenticator" below).
  5. The server spawns a new FWD client JVM with the startup procedure emain.p and the option client:web:embedded=true. The client starts its own web server and reports its URL; the server returns it to the browser as JSON.
  6. p2j.emain sets this URL as the src of the iframe (#embeddedP2J) and calls p2j.embedded.init(...). The FWD page in the iframe connects its websocket to the client and does the PAGE_LOADED handshake with the host page.
  7. p2j.emain calls the 4GL procedure doLogin in emain.p with the same user name and password. Hotel GUI's emain.p:
procedure doLogin.
   def input param messageId as int.
   def input param username as char.
   def input param password as char.
   if setuserid(username, password)
   then do:
      fwdInitialized = true.
      P2J-REMOTE-CALL('~{ "api":"p2jInitialized" ~}').
   end.
   else do:
      P2J-REMOTE-CALL('~{ "api":"showError" , "error":"Invalid username or password" ~}').
      quit.
   end.
end.

8. The p2jInitialized up-call reaches the host page, which now configures the shell and shows the first window (see "Configuring the shell").

Auto-login (SSO without a login form) is not available in embedded mode yet.

The SSO authenticator

The authenticator is a class in the application which implements com.goldencode.p2j.security.SsoAuthenticator, configured in /security/config/auth-mode (ssoplugin for the class name, optionally ssopluginoption / ssopluginconfig). It must implement:

Method Purpose
setOption(String option) Receives the text of ssopluginoption, right after the authenticator is created and before any authentication. Hotel GUI uses it for the name of the FWD account.
getLoginPage(String loadedTemplate) Returns the login page to serve, given the HTML template loaded from the application's resources (or null).
authenticate(Map<String, String[]> paramMap, Cookie[] cookies, LicensingData licensingData) Validates the posted credentials (usr, psw) and returns a SsoAuthenticator.Result: on success, with the name of the FWD account to run the client as (and optionally an auth blob, available as SESSION:AUTH-BLOB, and a cookie); on failure, with the HTTP code and the message to show.
logout(Cookie[] cookies) Invalidates the web session of the authentication cookie and returns a cookie with an invalid value (or null).

Other methods have default implementations and are optional: setConfig, initialize / terminate (server start and stop), getAuthCookieName and authenticate(Cookie[], LicensingData) (auto-login, not available in embedded mode), isTokenActive / refreshToken (token based providers).

OS user: the FWD client JVM is spawned under an OS user. After a successful authentication, the server uses:
  • the OS user and password set in the Result by the authenticator, if any;
  • otherwise, the OS user associated with the FWD account in the directory. The FWD account returned by the authenticator must therefore exist in the directory and be mapped to an OS user;
  • the shared webClient/defaultOsUser instead, when webClient/override is set or no OS user was found.

If no OS user can be determined, the client is not spawned and the login fails.

Session exit

  • Logout: p2j.embedded.logout() calls inMsg_logout in fwd-embedded-driver.p, which sets fwd-user-logout, runs the optional fwdLogout procedure (see "Integration Points in the 4GL Code") and executes QUIT.
  • 4GL QUIT (or the end of emain.p): emain.p sends the notifyDisconnect up-call before ending, and p2j.emain resets the host page.
  • Browser closed, network lost: the websocket closes. The FWD client waits webClient/watchdogTimeout milliseconds (default 120000) for the browser to reconnect, then exits.

When the client ends, the server releases the session's resources (ports, client registration).

How the code knows it runs embedded

  • Server side, 4GL: com.goldencode.p2j.ui.LogicalTerminal:isEmbedded(), called directly from 4GL (FWD's direct Java access). The answer is cached, so it can be called as often as needed. Hotel GUI wraps it in abl/embedded-mode.i, guarded so that the same source still compiles in OpenEdge:
FUNCTION is-embedded RETURNS LOGICAL:

&IF DEFINED(FWD-VERSION) &THEN
   IF com.goldencode.p2j.ui.LogicalTerminal:isEmbedded() THEN
      RETURN TRUE.
&ENDIF

   RETURN FALSE.

END FUNCTION.
  • 4GL shared variable: emain.p defines fwd-embedded-mode (NEW GLOBAL SHARED, initial value TRUE).
  • FWD runtime: the client driver (GuiWebDriver.isEmbeddedClient()) and ThinClient.isEmbedded(). FWD uses them to turn off the window decorations, to choose the embedded configuration nodes (dynamic layout, enhanced browse), to send the status and message text to the host page, and to reject the embedded-only APIs in other clients.

The JavaScript Side

Files

File Owner Purpose
p2j.embedded.js FWD (core) Host-side protocol and window controller: the postMessage channel to the iframe, the up-call APIs, running fwd-embedded-driver.p, the per-window state (pages, toolbar, menu bar, current page, status and messages), modality overlay.
p2j.emain.js FWD (core) Common host page logic: login, loading overlay, title, status and message areas, disconnect, window switch and close. Works with fixed element IDs in the host page (see below).
p2j.module.js, p2j.menu.js, p2j.toolbar.js, p2j.chips.js FWD (default renderers) Renderers for the module tree, the menu bar, the toolbar and the list of open windows. Used when the application does not give its own handler. The .classic.js variants render the same with Dojo dijit widgets.
emain.js (in custom/) application The custom emain object: the application's hooks called by p2j.emain (see "Configuring the shell").
other files in custom/ application The application's own pages, window scripts, CSS, images. Hotel GUI: ehotel.js, ehotel.css, calendar.js, aster.js.

The core files are not meant to be changed by the application. The application customizes the behavior through:

  • the custom emain object passed to p2j.emain.initialize(customEmain);
  • the configuration passed to p2j.embedded.initializeEmbeddedWindows(cfg), which can also replace the default renderers with the application's own handlers (see Customizing FWD JavaScript Modules for the interface each handler must implement);
  • window scripts, one per 4GL window, registered with p2j.emain.registerActiveWindow(windowId, script);
  • its own HTML, CSS and JS around the iframe.

Adding web content

New web content can be added to an embedded application in more than one place. Hotel GUI uses the two most common ones, compared below:

In the host page Inside the iframe, in an HTML-BROWSER widget
Hotel GUI example the calendar (heat map) and the aster plot of room utilization (ehotel.js, calendar.js, aster.js) the dashboard page of main-window.w (abl/dashboard-frame.w, webres/dashboard/index.html, dashboard.js)
Where it is shown in the host page, outside the iframe, next to the 4GL window inside a 4GL frame, as a widget of the 4GL window (Hotel GUI: an ADM page)
Served by the FWD server, from the application jar (/embedded/custom/...) the FWD client's own Jetty, from the client's working directory
Deployed by prepare.embedded (into the application jar, see "Packaging") deployapp.prepare (run by deploy.prepare), which copies webres/dashboard as loose files to deploy/client/webres/dashboard
Talks to the 4GL code with the embedded up-call APIs: P2J-REMOTE-CALL, p2j.embedded.invoke*, publish/subscribe (see "Up-calls") HTML-BROWSER messaging (see below)

HTML-BROWSER content (Hotel GUI's dashboard-frame.w):

  • The 4GL code creates the widget, points its embedded Jetty at a real directory and opens the page:
    file-info:file-name = "webres/dashboard".
    resBase = file-info:full-pathname.
    
    create html-browser hBrowser
       assign name          = "browser" 
              frame         = frame fMain:handle
              visible       = true
              width-pixels  = frame fMain:width-pixels
              height-pixels = frame fMain:height-pixels.
    
    assign
       hBrowser:resource-base = resBase
       hBrowser:context-path  = "/dashboard".
    
    pageOpened = hBrowser:open-web-page("index.html").
    
  • The page must be served by the client's Jetty (RESOURCE-BASE / CONTEXT-PATH). A URL on the server's web root (e.g. /webres/dashboard/index.html) does not work: the FWD page in the iframe comes from the client process, so such a URL is resolved against the client's host and port and is not found.
  • FILE-INFO resolves the relative name against the client's working directory (clientConfig/workingDir in the directory; deploy/client in Hotel GUI), which is where deployapp.prepare puts the files.
  • 4GL to JavaScript: hBrowser:POST-MESSAGE(text); the page receives it with window.addEventListener("message", ...).
  • JavaScript to 4GL: the page calls parent.postMessage(object, "*"). FWD delivers it as a COM-style event to the internal procedure <widget name>.WebBrowser.OnWebMessageReceived (Hotel GUI: browser.WebBrowser.OnWebMessageReceived in dashboard-frame.w), with the parameters INPUT p-url AS CHARACTER and INPUT p-data AS LONGCHAR (the message as JSON text).
  • Post a JavaScript object, not a JSON string: FWD applies JSON.stringify to the posted data, so a string would arrive double-encoded.
  • Hotel GUI builds a request/reply protocol on top of this: each message carries an id and an op, the reply carries the same id, and dashboard.js applies a timeout.

Loading the host page

Hotel GUI's web_partial_login_embedded.html loads the libraries with plain <script> tags, then Dojo, then the AMD modules (fwd/... is /embedded/fwd/, custom/... is /embedded/custom/):

var p2j = {};
define.amd.jQuery = true;
require(["fwd/p2j.embedded", "custom/emain", "fwd/p2j.emain", "dojo/dom", "dojo/dom-construct", "dojo/on", "dojo/domReady!"],
  function(embedded, oem, pemain, dom, domConstruct, on) {
     p2j.embedded = embedded;
     p2j.emain = pemain;
     emain = oem;
     p2j.emain.initialize(oem);

p2j.embedded, p2j.emain and $ (jQuery) are used as globals by the FWD modules.

p2j.emain expects these elements in the host page:

Element Used for
#p2jIframeParent, #embeddedP2J the iframe and its container
#mainView, #bgFader, #waitMsg the loading overlay
#windowTitle the title of the current 4GL window
#iframe-menubar the menu bar container
#status-area, #message-area the 4GL status and message text
.for-login, .for-runtime elements shown only on the login form, or only after login

Configuring the shell

After the p2jInitialized up-call, the host page calls p2j.embedded.initializeEmbeddedWindows(cfg). The configuration entries are described in Configuration. Hotel GUI (custom/emain.js) renders the 4GL menu bar, builds its own window list and page tabs, and moves the status and message areas into the host page:

me.p2jInitialized = function(msg)
{
   initializeSplitBars();

   var cfg =
   {
      "menubar" :
      {
         "container" : "fwdMenu" 
      },
      "initCallback"    : function() { p2j.emain.showCurrentWindow(p2j.embedded.getCurrentWindow()); },
      "populateWindows" : populateWindows,
      "populatePages"   : populatePages,
      "windowDestroyed" : p2j.emain.windowDestroyed,

      "embedded-status-area"    : true,
      "show-status-callback"    : p2j.emain.showStatus,

      "embedded-message-area"   : true,
      "show-messages-callback"   : p2j.emain.showMessages
   };

   p2j.embedded.registerIncomingRequest("resizeClientArea", resizeClientArea);
   resizeClientArea({ "full" : "yes" });

   p2j.embedded.initializeEmbeddedWindows(cfg);
};

A handler for the module tree, menu bar, toolbar or window list can render the JSON it receives with any web technology (e.g. a React component).

The custom emain object can implement these hooks; p2j.emain calls each one only if it exists:

Hook Called
p2jInitialized(msg) after the 4GL login succeeded
loadWindowScript(windowId, title) before a window is shown, to load its window script; returns true when it handled the window
windowCreated after a window was created
admPageChanged(pageId, element, userClick), admPageRefresh(windowId, pageId) when the current page of the window changed or must be refreshed
waitForInput when the 4GL code waits for user input
showLoading, hideLoading, getLoadingDelay loading overlay
getDefaultBgColor the background color to force on the 4GL windows

A window script is registered with p2j.emain.registerActiveWindow(windowId, script). FWD (p2j.emain) calls only two of its methods, each one only if it exists:

  • windowCreated(): after the window was created and shown;
  • windowDestroyed(): when the window was destroyed.

Any other method of a window script is an application convention, called by the application's own code. Hotel GUI's custom/emain.js (in its populatePages and page-click handlers) calls getPageButton(pageNum, title), populateCustomPages(container) and showEmbeddedPage(pageId) on the window script, if present. Hotel GUI's ehotel.js is the window script of main-window.w: it implements getPageButton (the page tabs with their icons, including the "Dashboard" tab), and windowCreated / windowDestroyed, which show and hide the calendar and the aster plot in the host page and release the window's handles and subscriptions. The dashboard page itself is a 4GL frame with an HTML-BROWSER (see "Adding web content").

Navigation

The data for the navigation widgets comes from the 4GL side; the JSON formats are described in Embedded widgets.

  • The window list comes from adm_windows.json. After the 4GL login, p2j.embedded loads it, runs fwd-embedded-driver.p persistently and calls populateWindows. Hotel GUI fills a <select> with the windows (shown only when there is more than one).
  • The module tree comes from the optional fwdGetModules function (see "Integration Points in the 4GL Code"). When the application has no modules, the window list is used. When the user chooses a module, the driver resolves the program to run and the configured module.event is called.
  • The pages are the tabs of an ADM folder. When a window is shown, the driver returns its pages and populatePages builds the tabs. A click on a tab calls p2j.embedded.showWindowPage(pageId, callback). When the 4GL code changes the page itself, the host page is notified with the admPageChanged up-call; this needs the ADM code to publish fwd-adm-page-changed (Hotel GUI patched abl/common/adm2/containr.p).
  • The menu bar is removed from the 4GL window and rendered as HTML by the host page. Choosing a menu item applies the right event to the 4GL menu item (CHOOSE, VALUE-CHANGED for toggle-box items, MENU-DROP). Toolbar buttons work the same way (CHOOSE).

What the host page draws

In embedded mode FWD draws only the inside of the 4GL window: its frames and widgets. Everything a desktop window would have around it is replaced by HTML in the host page, which the application builds and styles as it likes:

Desktop 4GL window Embedded mode: drawn by the host page How
title bar any element showing the title p2j.emain.setTitle writes the title of the current window into #windowTitle; the title comes from adm_windows.json (see "Conversion") or from the 4GL window
close button a close button, e.g. an "x" in a title bar or on a chip in the window list call p2j.emain.removeWindow(windowId), which applies WINDOW-CLOSE to the 4GL window, so the 4GL code closes it as usual
minimize / maximize buttons not needed the iframe shows one window at a time; its size is decided by the host page (see "Sizing and resizing")
switching between windows (task bar) a window list: a <select>, tabs, chips populateWindows, the chipset configuration, p2j.emain.switchWindow
menu bar an HTML menu, e.g. opened from a "hamburger" button the 4GL menu bar is serialized to JSON and rendered as HTML by the menubar handler (by default p2j.menu.js, which builds the menu items; the button which opens the menu is part of the host page)
folder tabs (ADM pages) HTML tabs populatePages, p2j.embedded.showWindowPage
status and message areas any HTML element embedded-status-area / embedded-message-area and their callbacks
- application controls, e.g. logout plain HTML/CSS/JS; logout calls p2j.embedded.logout

A typical layout: a title bar in the host page with a "hamburger" button on the left (opening the 4GL menu bar rendered by p2j.menu.js), the window title in the middle and a close button on the right (calling p2j.emain.removeWindow); below it, the iframe with the 4GL window. Hotel GUI's host page uses a Bootstrap navigation bar with the window list, the page tabs and a Logout tab; it has no title element and no close button, so the main window stays open until the user logs out.

Controlling what is in the iframe

The way the driver manages the ADM windows (hidden widgets, relocated frames, nested modal dialogs, ADM events) is described in Management of ADM windows. The main flows:

Showing a window (p2j.emain.showCurrentWindow(windowId, title, pageNum)):

  1. customEmain.loadWindowScript loads the window script, if the application has one.
  2. p2j.embedded.showWindow calls inMsg_fwdShowWindow in the driver, which:
    1. checks the access (fwdCheckAccess, if defined);
    2. runs the program persistently (or calls fwdRunModule, if defined);
    3. prepares the window for embedded mode: removes its menu bar, status area and message area, and hides the widgets returned by fwdGetHiddenEmbeddedWidgets (e.g. the folder tab strip, which is replaced by the host page's tabs);
    4. calls fwdWindowInitialized, if defined; unless it returns TRUE, initializes the ADM object (initializeObject for ADM2, dispatch("initialize") for ADM1);
    5. initializes the current page: either the default page processing of the driver, or, when initEmbeddedPageCustom is defined, that procedure instead (see "Integration Points in the 4GL Code");
    6. returns the window ID and the pages.
  3. The host page builds the pages, the menu bar, the toolbar and the status and message areas.

Switching windows: p2j.embedded.switchWindow(windowId, callback) calls inMsg_fwdSwitchWindow, which moves the window to the top and gives it the focus; the host page rebuilds the pages, menu bar, toolbar, status and messages of that window. When the 4GL code itself brings another window to the top, the FWD page in the iframe reports it and the host page switches too.

Closing a window:

  • from the host page: p2j.emain.removeWindow(windowId) switches to the window and applies WINDOW-CLOSE to it, so the 4GL code closes it as usual (its triggers run);
  • from the 4GL code: when a window is destroyed, the iframe notifies the host page, which calls inMsg_fwdDestroyWindow (to destroy the ADM object) and then the windowDestroyed callback. Hotel GUI's window script releases its handles and subscriptions there.

Modal dialogs: when the 4GL code opens a modal dialog, the host page covers all its elements, except the iframe, with a semi-transparent overlay (the CSS class given to p2j.embedded.init; Hotel GUI: .embedded-overlay), so the user can not use the host page's navigation until the dialog is closed.

Sizing and resizing

  • The desktop of the embedded client is the iframe: the size of the iframe is sent to the FWD client when the client starts and again after each resize of the browser window (debounced, 500 ms).
  • When the iframe is resized, the 4GL windows which are maximized are resized to the new desktop size.
  • With dynamic-layout/embedded/window-maximize set to TRUE (under /server/default/runtime/default/), the current window is always resized to fill the iframe. window-maximize is read only when dynamic-layout/embedded/enabled is also TRUE, which also turns on the dynamic layout for the embedded client; with enabled = FALSE, window-maximize has no effect. (Hotel GUI's directory.xml.template ships enabled = FALSE and window-maximize = TRUE, so its window is not maximized.) Both are needed:
<node class="container" name="dynamic-layout">
  <node class="container" name="embedded">
    <node class="boolean" name="enabled">
      <node-attribute name="value" value="TRUE"/>
    </node>
    <node class="boolean" name="window-maximize">
      <node-attribute name="value" value="TRUE"/>
    </node>
  </node>
</node>
  • Resizing a window does not resize its frames and widgets: they keep the geometry from the 4GL code. FWD's dynamic layout only grows browse widgets. Any other widget only changes size when the 4GL code resizes it.
  • The iframe's size is decided by the host page (CSS). The 4GL code can ask the host page for a different size with an up-call. Hotel GUI's main-window.w tells the host page how much room the selected page needs, and maximizes the window for the dashboard page:
P2J-REMOTE-CALL('~{ "api":"resizeClientArea"' +
                ', "width":'  + TRIM(STRING(pxW)) +
                ', "height":' + TRIM(STRING(pxH)) +
                ', "full":"'  +
                (IF piPageNum EQ {&DASHBOARD-PAGE} THEN "yes" ELSE "no") +
                '" ~}').

The host page's resizeClientArea handler (Hotel GUI custom/emain.js) changes the iframe container's CSS accordingly.

Theming

Part Styled by
the host page (header, navigation, tabs, footer, dashboards, the module tree, menu bar and toolbar rendered as HTML) the application's CSS
the 4GL windows, frames and widgets in the iframe the FWD theme (see Themes); MaterialTheme is designed for embedded mode
the background of the 4GL windows customEmain.getDefaultBgColor(): the color is forced on the window and its main frame when it is shown

The status and the message areas can be moved into the host page (embedded-status-area, embedded-message-area), where they are styled with CSS like any other HTML.

Integration Points in the 4GL Code

The driver calls these optional APIs in the program referenced by the shared variable fwd-custom-apis (by default emain.p; it can be changed to another persistent program). Each one is used only when the program defines it. The full signatures are documented in the header of Hotel GUI's abl/emain.p (note that its descriptions of initEmbeddedPageCustom and fwdWindowInitialized are inaccurate; the table below describes the actual behavior); initEmbeddedPageCustom, fwdListToolbar and inMsg_fwdToolbarItemSelected are also described in Embedded widgets.

API Kind Purpose
fwdLogout procedure Additional logic when the user logs out.
fwdGetModules function, returns CHARACTER The module tree, as a JSON array.
fwdGetProgramModule procedure The program to run for a module.
fwdCheckAccess function, returns LOGICAL Application-specific security check before a module is run.
fwdRunModule function, returns HANDLE Runs the module's program persistently, when it needs custom code to start.
initEmbeddedPageCustom procedure Replaces the driver's default page processing (it does not extend it). When defined, it must itself select the page, hide the widgets which must not be shown (e.g. the folder tab strip), move frames as needed and return the pages. It is called each time a page is initialized: when the window is shown, when the page changes and when the page is refreshed.
fwdListToolbar function The toolbar buttons, as a JSON array.
fwdGetToolbarFrame function, returns HANDLE The frame holding the toolbar (overrides the converted function with the same name).
fwdWindowInitialized function, returns LOGICAL Not a notification: called before the ADM initialization of the window. Returning TRUE means the application has initialized the window itself, and the driver skips initializeObject (ADM2) / dispatch("initialize") (ADM1). Return FALSE to let the driver initialize it.
inMsg_fwdToolbarItemSelected procedure Custom handling of a toolbar button click.

Other places where the application code usually needs changes:

  • ADM page changes: publish fwd-adm-page-changed from the ADM code when the page changes (see "Navigation").
  • Code which only makes sense in one mode: guard it with is-embedded() (see "How the code knows it runs embedded"). Hotel GUI includes embedded-mode.i in its windows and frames.
  • Up-calls: the procedures called from the host page, and the P2J-REMOTE-CALL statements which send data to it (see "Up-calls").
  • Directory: a remoteentrypoint ACL for each external program run from the host page. A missing ACL ends the session (see "JavaScript to 4GL").

Up-calls

Up-calls connect the host page and the 4GL code in both directions. The APIs are described in API; this section explains how they behave.

Direction API Synchronous?
4GL -> JavaScript P2J-REMOTE-CALL(json) yes: the 4GL code waits for the host page's answer
JavaScript -> 4GL p2j.embedded.invoke*(...) no: the result comes in a callback
both PUBLISH / SUBSCRIBE with p2j.embedded.publish / subscribe no

All the values passed are strings.

4GL to JavaScript: P2J-REMOTE-CALL

P2J-REMOTE-CALL(<character expression>) is an FWD extension of the 4GL language: a function which sends a string to the host page and returns the host page's answer (CHARACTER). It can also be used as a statement, ignoring the answer. Since OpenEdge does not know it, code which must also compile in OpenEdge needs the &IF DEFINED(FWD-VERSION) guard.

By convention the string is a JSON object with an "api" property; p2j.embedded dispatches it to the handler registered for that name with p2j.embedded.registerIncomingRequest(api, handler, parser). By default, the host page answers "OK" after the handler ran; the handler's return value is not sent back.

To pass data from the host page to the 4GL code, do not rely on the answer of P2J-REMOTE-CALL: let the handler call a 4GL procedure or function with p2j.embedded.invoke*(...) (see "JavaScript to 4GL"), which runs when the 4GL code next waits for input. The same request/answer pattern, in the other direction, is shown in "Example: request and answer".

p2j.embedded.setMessageCallback(callback) also exists, but it replaces the default dispatch. With a message callback installed, p2j.embedded no longer calls the handlers registered with registerIncomingRequest, including FWD's own handlers (p2jInitialized, notifyDisconnect, admPageChanged, ...). The callback must dispatch every message itself and must answer every P2J-REMOTE-CALL with p2j.embedded.sendRemoteCallResponse(messageId, answer); otherwise the 4GL session waits forever. It is not recommended for normal use.

Rules:

  • The 4GL code waits until the host page answers, without a timeout. A JS handler which throws an exception can leave the 4GL session waiting forever: keep the handlers short and catch their errors.
  • P2J-REMOTE-CALL in a client which is not embedded (Swing, Virtual Desktop, ChUI) ends the session. Guard it with is-embedded().
  • The JSON is built by the 4GL code as a string (quote and escape the values; ~{ and ~} in 4GL string literals).

JavaScript to 4GL

The host page runs 4GL code with p2j.embedded.invokeExternalProgram, invokeExternalProgramPersistent, invokeProcedure and invokeFunction, and can apply a 4GL event to a widget with applyEvent.

  • args is an object whose keys are the 4GL parameter names (not case-sensitive; the order does not matter). Every value is sent as a string and converted to the parameter's type; a missing parameter gets the unknown value. Supported parameter types: CHARACTER, LONGCHAR, INTEGER, INT64, DECIMAL, LOGICAL, DATE, DATETIME, DATETIME-TZ, RAW, RECID, ROWID. HANDLE, TABLE, DATASET, object and EXTENT parameters are not supported. Dates must be in the session's date format.
  • The call is queued and runs when the 4GL code waits for user input (WAIT-FOR, PROCESS EVENTS, a modal dialog), like a trigger. While the 4GL code is busy, the calls wait.
  • An ERROR raised by the 4GL code is reported to the callback. By default it is not raised again in the 4GL session: p2j.emain calls p2j.embedded.silentErrorEnable() before the login. Only when the application turns silent errors off (p2j.embedded.silentErrorDisable()) is the ERROR raised again in the 4GL session after being reported. STOP and QUIT are always raised again.
  • Security: every external program run from JavaScript must be allowed by a remoteentrypoint ACL (see Allow the embedded app to invoke 4GL code), and internal procedures and functions can only be called in a handle obtained from JavaScript (invokeExternalProgramPersistent).
  • A security failure ends the session: a missing remoteentrypoint ACL, or a call in a handle which was not obtained from JavaScript, is logged (SEVERE) in the server log and throws a SilentUnwindException, which unwinds the 4GL session. No access error is reported to the JavaScript callback.

Publish and subscribe

4GL named events also work between the two sides (see Managing named events). Hotel GUI publishes roomUtilization from rsrvdtrg.p (PUBLISH ... GLOBAL) to ehotel.js, and ehotel.js publishes capacityZoom to avail-rooms-frame.w (SUBSCRIBE TO "capacityZoom" ANYWHERE).

Example: request and answer

Hotel GUI's calendar (web content) asks the 4GL code for the room utilization in a date range. The answer is sent back by a second up-call, with the messageId of the request; this request/answer pattern is an application convention.

1. JavaScript runs ehotel.p persistently and keeps its handle (ehotel.js):

p2j.embedded.invokeExternalProgramPersistent(function(msg)
{
   if (msg.payload.error)
   {
      dbg(msg.payload["error-num"] + " " + msg.payload["error-msg"]);
   }

   remoteProgramHandle = msg.payload.handle;
   ...
},
remoteProgram, {});

2. JavaScript registers the handler for the answer, and sends the request. p2j.emain.invoke calls the procedure "inMsg_" + apiName and adds a messageId argument:

p2j.embedded.registerIncomingRequest("utilizationByDateResponse", utilizationByDateResponse, parseUtilizationByDateResponse);
...
function getUtilizationByDate(start, end)
{
   var args =
   {
      "start-date" : start,
      "end-date"   : end
   };

   p2j.emain.invoke(remoteProgramHandle, "utilizationByDate", args);
};

3. The 4GL procedure receives the request; the parameter names match the keys of args (ehotel.p):

procedure inMsg_utilizationByDate.

   def input param messageId as int.
   def input param start-date as date.
   def input param end-date as date.

   run sendMsg_utilizationByDate(messageId, start-date, end-date).

end.

4. sendMsg_utilizationByDate reads the database, builds the JSON answer and sends it:

   jsonMsg = "~{~n".
   jsonMsg = jsonMsg + ' "api":"utilizationByDateResponse",~n'.
   jsonMsg = jsonMsg + ' "messageId":"' + string(messageId) + '",~n'.
   jsonMsg = jsonMsg + ' "rows": [' + row-data + '],~n'.
   jsonMsg = jsonMsg + ' "domain": [ 0.0, 1.0 ],~n'.
   jsonMsg = jsonMsg + ' "start":"' + string(start-date, "99/99/9999") + '",~n'.
   jsonMsg = jsonMsg + ' "end":"' + string(end-date, "99/99/9999") + '"~n'.
   jsonMsg = jsonMsg + "~}".

   P2J-REMOTE-CALL(jsonMsg).

5. parseUtilizationByDateResponse and utilizationByDateResponse (ehotel.js) receive the data and draw the calendar.

Development Process

Making changes

All embedded content is served from the jars, so editing a source file has no effect until the jar is rebuilt and deployed. With the Hotel GUI build:

Change Steps
4GL code ant deploy (clean, convert, jar, deploy.prepare); restart the server. Conversion also regenerates adm_windows.json.
Application JS, CSS, images (embedded/src/resources/custom/) ant jar deploy.prepare; restart the server; reload the page without the browser cache. Do not use ant deploy for this: it also deletes and reconverts the converted code.
Host page (webres/web_partial_login_embedded.html) same as above
HTML-BROWSER content (webres/dashboard/) ant deploy.prepare (or deployapp.prepare), which copies the files to deploy/client/webres/dashboard; then reopen the page. These files are read from disk by the client's Jetty, not from the jar.
FWD JS (p2j.embedded.js, p2j.emain.js, ...) build FWD (./gradlew jar); ant deploy.prepare in the project; restart the server; reload without cache.
Directory (deploy/server/directory.xml) restart the server

The FWD JS and CSS served under /embedded/fwd/ and /embedded/custom/ have no cache headers, so use a hard refresh, or keep "Disable cache" on in the browser's developer tools while working.

Debugging

  • Browser: the developer tools of the browser. The host page and the iframe are separate documents: choose the right one (the JavaScript context) in the console.
  • Logs: in deploy/logs/: fwd_server_* (FWD server), fwd_client_* (one per web client), spawn_console*. Loggers are configured in the directory, under logging/loggers; e.g. com.goldencode.p2j.main.EmbeddedWebHandler (resources not found).
  • Java: server.sh -d starts the server with a debugger port; the web clients get theirs through clientConfig/jvmArgs.

Stopping at a breakpoint can disconnect the client:

  • paused in the browser: the websocket is closed when it stays idle longer than webClient/webSocketTimeout (default 90000 ms); the FWD client then waits webClient/watchdogTimeout (default 120000 ms) for a reconnect and exits;
  • paused in the FWD client JVM: the browser closes the websocket and reconnects after some lost pings (based on webClient/maxIdleTime and webClient/pingPongInterval).

Raise these values in the development environment when debugging step by step.

Troubleshooting

Symptom Likely cause
/embedded is not found webClient/embedded is not TRUE, or no SSO authenticator is configured
A JS or CSS change has no effect the jar was not rebuilt and deployed, the server was not restarted, or the browser used its cache
A changed file in embedded/src/resources/fwd/ has no effect that folder is not used: the FWD JS comes from p2j.jar
A window is missing from the window list it is not an ADM window, add-embedded-windows is not set, or it was removed from adm_windows.json
The session ends when JavaScript calls 4GL code (a SEVERE "Access to remote program ... not allowed" or "persistent procedure which was not obtained by the remote ... application" in the server log) the program has no remoteentrypoint ACL for the user's FWD account, or the procedure was called in a handle not obtained from JavaScript; no error is reported to the callback
The login fails after the authenticator accepted the user the FWD account returned by the authenticator does not exist in the directory or has no OS user, and no webClient/defaultOsUser applies
An ERROR in 4GL code called from JavaScript is not raised in the 4GL session silent errors are enabled by default (p2j.emain calls silentErrorEnable()); the error is only reported to the callback
FWD's own up-calls (p2jInitialized, notifyDisconnect, admPageChanged) stop working a message callback was installed with setMessageCallback, which replaces the default dispatch
window-maximize has no effect dynamic-layout/embedded/enabled is not TRUE
After adding initEmbeddedPageCustom, the page is not selected or the folder tabs appear in the iframe initEmbeddedPageCustom replaces the driver's page processing; it must select the page, hide the widgets and return the pages itself
fwdWindowInitialized is defined and the window is not initialized it returned TRUE, which skips the ADM initialization
A JavaScript call to 4GL never runs the 4GL code is busy and never waits for input
The 4GL session hangs on P2J-REMOTE-CALL the host page never answered, e.g. the handler threw an exception
The session ends at a P2J-REMOTE-CALL the code runs in a client which is not embedded
Parameter mismatch error on an up-call a key in args is not a parameter name, a value can not be converted, or the parameter type is not supported
The client disconnects while debugging websocket and watchdog timeouts (see "Debugging")

Limitations

  • An SSO authenticator is required; auto-login is not available in embedded mode yet.
  • One 4GL window is shown at a time; the 4GL windows have no title bar or border, the host page must provide the window switching.
  • Frames and widgets are not resized with the window; only browse widgets grow with the dynamic layout. Fitting the legacy screens to the available space needs 4GL code.
  • The 4GL widgets are drawn by FWD on a canvas: the host page's CSS can not style them (use a FWD theme).
  • Up-call values are strings only; HANDLE, TABLE, DATASET, object and EXTENT parameters are not supported.
  • A JavaScript call which fails the security checks (missing remoteentrypoint ACL, handle not obtained from JavaScript) ends the 4GL session; it is not reported to the callback as an error.
  • P2J-REMOTE-CALL:
    • waits for the host page's answer, without a timeout. The 4GL session is blocked until the answer comes;
    • hangs the session when no answer comes: the 4GL code sent invalid JSON, the parser or the handler threw an exception, or a custom message callback did not call sendRemoteCallResponse;
    • ends the session in a client which is not embedded (Swing, ChUI, Virtual Desktop). Guard it with is-embedded().
  • JavaScript calls to 4GL only run when the 4GL code waits for input.
  • Every change to the embedded web content needs a rebuild and a server restart.

© 2026 Golden Code Development Corporation. ALL RIGHTS RESERVED.