@eclipse-scout/core
Version:
Eclipse Scout runtime
1,112 lines (1,094 loc) • 5.71 MB
JavaScript
import * as __WEBPACK_EXTERNAL_MODULE_reflect_metadata_lite_5c49451e__ from "reflect-metadata/lite";
import * as __WEBPACK_EXTERNAL_MODULE_sourcemapped_stacktrace_254f05bd__ from "sourcemapped-stacktrace";
/******/ var __webpack_modules__ = ({
/***/ "./src/App.ts":
/*!********************!*\
!*** ./src/App.ts ***!
\********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ App: () => (/* binding */ App)
/* harmony export */ });
/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index */ "./src/index.ts");
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! jquery */ "jquery");
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_1__);
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
let instance = null;
let listeners = [];
let bootstrappers = [];
class App extends _index__WEBPACK_IMPORTED_MODULE_0__.EventEmitter {
static addListener(type, handler) {
let listener = {
type: type,
func: handler
};
if (instance) {
instance.events.addListener(listener);
} else {
listeners.push(listener);
}
return listener;
}
/**
* Adds a function that needs to be executed while bootstrapping.
* @see AppModel.bootstrappers
*/
static addBootstrapper(bootstrapper) {
if (bootstrappers.indexOf(bootstrapper) > -1) {
throw new Error('Bootstrapper is already registered.');
}
bootstrappers.push(bootstrapper);
}
/**
* The response of a successful ajax call with status 200 may contain a {@link JsonErrorResponse}.
* This method detects this case and throws an error containing the error details of the response together with the given request url.
* @throws JsonErrorResponseContainer
* @returns the given data as it is if it does not contain an error
*/
static handleJsonError(url, data) {
if (data?.error) {
// The result may contain a json error (e.g. session timeout) -> abort processing
throw {
error: data.error,
url: url
};
}
return data;
}
static get() {
return instance;
}
static _set(newApp) {
if (instance) {
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isWarnEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.warn('Overwriting already existing App "' + instance + '" with "' + newApp + '".');
}
instance = newApp;
}
remote;
initialized;
sessions;
errorHandler;
version;
bootstrappers;
_loadingTimeoutId;
constructor() {
super();
this.remote = false;
this.initialized = false;
this.sessions = [];
this.bootstrappers = [];
this._loadingTimeoutId = null;
// register the listeners which were added to scout before the app is created
listeners.forEach(listener => {
this.addListener(listener);
});
listeners = [];
App._set(this);
this.errorHandler = this._createErrorHandler();
}
/**
* Main initialization function.
*
* Calls {@link _prepare}, {@link _bootstrap} and {@link _init}.<br>
* At the initial phase the essential objects are initialized, those which are required for the next phases like logging and the object factory.<br>
* During the bootstrap phase additional scripts may get loaded required for a successful session startup.<br>
* The actual initialization does not get started before these bootstrap scripts are loaded.
*/
init(options) {
options = options || {};
return this._prepare(options).then(this._bootstrap.bind(this, options.bootstrap)).then(this._init.bind(this, options)).then(this._initDone.bind(this, options)).catch(this._fail.bind(this, options));
}
/**
* Initializes the logging framework and the object factory.
* This happens at the prepare phase because all these things should be available from the beginning.
*/
_prepare(options) {
return this._prepareLogging(options).done(() => {
this._prepareEssentials(options);
this._prepareDone(options);
});
}
_prepareEssentials(options) {
_index__WEBPACK_IMPORTED_MODULE_0__.ObjectFactory.get().init();
}
_prepareDone(options) {
this.trigger('prepare', {
options: options
});
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isDebugEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.debug('App prepared');
}
_prepareLogging(options) {
return _index__WEBPACK_IMPORTED_MODULE_0__.logging.bootstrap();
}
/**
* Executes the bootstrappers.
*
* The actual session startup begins only when all promises of the bootstrappers are completed.
* This gives the possibility to dynamically load additional scripts or files which are mandatory for a successful application startup.
*/
_bootstrap(options) {
options = options || {};
options.bootstrappers = options.bootstrappers || [];
this.bootstrappers = [...this._defaultBootstrappers(options), ...options.bootstrappers, ...this.bootstrappers, ...bootstrappers].filter(bootstrapper => !!bootstrapper);
return jquery__WEBPACK_IMPORTED_MODULE_1___default().promiseAll(this._doBootstrap()).catch(this._bootstrapFail.bind(this, options)).then(this._bootstrapDone.bind(this, options)); // BootstrapDone must only be executed if there are no boostrap errors
}
_defaultBootstrappers(options) {
return [_index__WEBPACK_IMPORTED_MODULE_0__.Device.get().bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.Device.get()), _index__WEBPACK_IMPORTED_MODULE_0__.fonts.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.fonts, options.fonts), _index__WEBPACK_IMPORTED_MODULE_0__.locales.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.locales, options.localesUrl), _index__WEBPACK_IMPORTED_MODULE_0__.texts.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.texts, options.textsUrl), _index__WEBPACK_IMPORTED_MODULE_0__.codes.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.codes, options.codesUrl), _index__WEBPACK_IMPORTED_MODULE_0__.access.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.access, options.permissionsUrl), _index__WEBPACK_IMPORTED_MODULE_0__.config.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.config, options.configUrl), _index__WEBPACK_IMPORTED_MODULE_0__.uiPreferences.bootstrap.bind(_index__WEBPACK_IMPORTED_MODULE_0__.uiPreferences)];
}
_doBootstrap() {
return this.bootstrappers.map(bootstrapper => bootstrapper());
}
_bootstrapDone(options) {
_index__WEBPACK_IMPORTED_MODULE_0__.webstorage.removeItemFromSessionStorage('scout:bootstrapErrorPageReload');
this.trigger('bootstrap', {
options: options
});
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isDebugEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.debug('App bootstrapped');
}
/**
* @param vararg may either be
* - an {@link AjaxError} for requests executed with {@link ajax} or {@link AjaxCall}
* - a {@link JQuery.jqXHR} for requests executed with {@link $.ajax}. The parameters `textStatus`, `errorThrown` and `requestOptions` are only set in this case.
* - a {@link JsonErrorResponseContainer} if a successful response contained a {@link JsonErrorResponse} which was transformed to an error (e.g. using {@link App.handleJsonError}).
*/
_bootstrapFail(options, vararg, textStatus, errorThrown, requestOptions) {
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isInfoEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info('App bootstrap failed');
// If one of the bootstrap ajax call fails due to a session timeout, the index.html is probably loaded from cache without asking the server for its validity.
// Normally, loading the index.html should already return a session timeout, but if it is loaded from the (back button) cache, no request will be done and therefore no timeout can be returned.
// The browser is allowed to display a page when navigating back without issuing a request even though cache-headers are set to must-revalidate.
// The only way to prevent it would be the no-store header but then pressing back would always reload the page and not only on a session timeout.
// Sometimes the JavaScript and therefore the ajax calls won't be executed in case the page is loaded from that cache, but sometimes they will nevertheless (we don't know the reasons).
// So, if it that happens, the server will either return a session timeout or a status 401 (Unauthorized) and the best thing we can do is to reload the page hoping a request for the index.html
// will be done which eventually will be forwarded to the login page.
// Additionally, requests may fail due to other various reasons, e.g. Chrome may report ERR_NETWORK_CHANGED or ERR_CERT_VERIFER_CHANGED.
// Since a page reload normally solves these issues as well, the reload is done on any error not just session timeouts.
let {
url,
message
} = this._analyzeBootstrapError(vararg, textStatus, errorThrown, requestOptions);
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isInfoEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info(`Error for resource ${url}. Reloading page...`);
if (_index__WEBPACK_IMPORTED_MODULE_0__.webstorage.getItemFromSessionStorage('scout:bootstrapErrorPageReload')) {
// Prevent loop in case reloading did not solve the problem
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isWarnEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.warn('Prevented automatic reload, startup will likely fail.');
_index__WEBPACK_IMPORTED_MODULE_0__.webstorage.removeItemFromSessionStorage('scout:bootstrapErrorPageReload');
throw new Error(`Bootstrap resource ${url} could not be loaded: ${message}`);
}
_index__WEBPACK_IMPORTED_MODULE_0__.webstorage.setItemToSessionStorage('scout:bootstrapErrorPageReload', 'true');
_index__WEBPACK_IMPORTED_MODULE_0__.scout.reloadPage();
// Make sure promise will be rejected with all original arguments so that it can be eventually handled by this._fail
// eslint-disable-next-line prefer-rest-params
let args = _index__WEBPACK_IMPORTED_MODULE_0__.objects.argumentsToArray(arguments).slice(1);
return jquery__WEBPACK_IMPORTED_MODULE_1___default().rejectedPromise(...args);
}
_analyzeBootstrapError(vararg, textStatus, errorThrown, requestOptions) {
let ajaxError;
let jsonError;
if (vararg instanceof _index__WEBPACK_IMPORTED_MODULE_0__.AjaxError) {
ajaxError = vararg;
} else if (jquery__WEBPACK_IMPORTED_MODULE_1___default().isJqXHR(vararg)) {
ajaxError = new _index__WEBPACK_IMPORTED_MODULE_0__.AjaxError({
jqXHR: vararg,
textStatus: textStatus,
errorThrown: errorThrown,
requestOptions: requestOptions
});
} else if (_index__WEBPACK_IMPORTED_MODULE_0__.objects.isObject(vararg) && vararg.error) {
jsonError = vararg;
}
let url;
let message;
if (ajaxError) {
// AJAX error
// If a resource returns 401 (unauthorized) it is likely a session timeout.
// This may happen for REST resources or if a reverse proxy returned the response
url = ajaxError.requestOptions?.url || '';
message = `${ajaxError.errorDo?.message || ''}`;
let httpStatus = `${this.errorHandler.formatAjaxStatus(ajaxError.jqXHR, ajaxError.errorThrown)}`;
if (!message.includes(httpStatus)) {
message = `${message} [${this.errorHandler.formatAjaxStatus(ajaxError.jqXHR, ajaxError.errorThrown)}]`.trim();
}
} else if (jsonError) {
// Json based error
// Json errors (normally processed by Session.js) are returned with http status 200
url = jsonError.url;
message = `${jsonError.error.message} [Code ${jsonError.error.code}]`;
}
return {
url,
message
};
}
/**
* Initializes a session for each html element with class '.scout' and stores them in scout.sessions.
*/
_init(options) {
options = options || {};
this.setLoading(true);
let compatibilityPromise = this._checkBrowserCompatibility(options);
if (compatibilityPromise) {
this.setLoading(false);
return compatibilityPromise.then(newOptions => this._init(newOptions));
}
this._initVersion(options);
this._prepareDOM();
this._installErrorHandler();
this._installGlobalMouseDownInterceptor();
this._installSyntheticActiveStateHandler();
this._ajaxSetup();
this._installExtensions();
this._triggerInstallExtensions();
return this._load(options).then(this._loadSessions.bind(this, options.session));
}
/**
* Maybe implemented to load data from a server before the desktop is created.
* @returns promise which is resolved after the loading is complete
*/
_load(options) {
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise();
}
_checkBrowserCompatibility(options) {
let device = _index__WEBPACK_IMPORTED_MODULE_0__.Device.get();
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isInfoEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info('Detected browser ' + device.browser + ' version ' + device.browserVersion);
if (!_index__WEBPACK_IMPORTED_MODULE_0__.scout.nvl(options.checkBrowserCompatibility, true) || device.isSupportedBrowser()) {
// No check requested or browser is supported
return;
}
let deferred = jquery__WEBPACK_IMPORTED_MODULE_1___default().Deferred();
let newOptions = _index__WEBPACK_IMPORTED_MODULE_0__.objects.valueCopy(options);
newOptions.checkBrowserCompatibility = false;
jquery__WEBPACK_IMPORTED_MODULE_1___default()('.scout').each(function () {
let $entryPoint = jquery__WEBPACK_IMPORTED_MODULE_1___default()(this);
let $box = $entryPoint.appendDiv();
$box.load('unsupported-browser.html', () => {
$box.find('button').on('click', () => {
$box.remove();
deferred.resolve(newOptions);
});
});
});
return deferred.promise();
}
setLoading(loading) {
if (loading) {
this._loadingTimeoutId = setTimeout(() => {
// Don't start loading if a desktop is already rendered to prevent flickering when the loading will be set to false after app initialization finishes
if (!this.sessions.some(session => session.desktop && session.desktop.rendered)) {
this._renderLoading();
}
}, 200);
} else {
clearTimeout(this._loadingTimeoutId);
this._loadingTimeoutId = null;
this._removeLoading();
}
}
_renderLoading() {
let $body = jquery__WEBPACK_IMPORTED_MODULE_1___default()('body'),
$loadingRoot = $body.children('.application-loading-root');
if (!$loadingRoot.length) {
$loadingRoot = $body.appendDiv('application-loading-root').addClass('application-loading-root').fadeIn();
}
_index__WEBPACK_IMPORTED_MODULE_0__.aria.role($loadingRoot, 'alert');
_index__WEBPACK_IMPORTED_MODULE_0__.aria.screenReaderOnly($loadingRoot.appendDiv('text').attr('lang', 'en-US').text('Loading'));
this._renderLoadingElement($loadingRoot, 'application-loading01');
this._renderLoadingElement($loadingRoot, 'application-loading02');
this._renderLoadingElement($loadingRoot, 'application-loading03');
}
_renderLoadingElement($loadingRoot, cssClass) {
if ($loadingRoot.children('.' + cssClass).length) {
return;
}
$loadingRoot.appendDiv(cssClass).hide().fadeIn();
}
_removeLoading() {
let $loadingRoot = jquery__WEBPACK_IMPORTED_MODULE_1___default()('body').children('.application-loading-root');
// the fadeout animation only contains a to-value and no from-value
// therefore set the current value to the elements style
$loadingRoot.css('opacity', $loadingRoot.css('opacity'));
// Add animation listener before adding the classes to ensure the listener will always be triggered even while debugging
$loadingRoot.oneAnimationEnd(() => $loadingRoot.remove());
if ($loadingRoot.css('opacity') === '1') {
$loadingRoot.addClass('fadeout and-more');
} else {
$loadingRoot.addClass('fadeout');
}
if (!_index__WEBPACK_IMPORTED_MODULE_0__.Device.get().supportsCssAnimation()) {
// fallback for old browsers that do not support the animation-end event
$loadingRoot.remove();
}
}
_initVersion(options) {
this.version = _index__WEBPACK_IMPORTED_MODULE_0__.scout.nvl(this.version, options.version, jquery__WEBPACK_IMPORTED_MODULE_1___default()('scout-version').data('value'));
}
_prepareDOM() {
_index__WEBPACK_IMPORTED_MODULE_0__.scout.prepareDOM(document);
}
_installGlobalMouseDownInterceptor() {
_index__WEBPACK_IMPORTED_MODULE_0__.scout.installGlobalMouseDownInterceptor(document);
}
_installSyntheticActiveStateHandler() {
_index__WEBPACK_IMPORTED_MODULE_0__.scout.installSyntheticActiveStateHandler(document);
}
/**
* Installs a global error handler.
*
* Note: we do not install an error handler on popup-windows because everything is controlled by the main-window
* so exceptions will also occur in that window. This also means, the fatal message-box will be displayed in the
* main-window, even when a popup-window is opened and active.
*
* Caution: The error.stack doesn't look the same in different browsers. Chrome for instance puts the error message
* on the first line of the stack. Firefox does only contain the stack lines, without the message, but in return
* the stack trace is much longer :)
*/
_installErrorHandler() {
window.onerror = this.errorHandler.windowErrorHandler;
// FIXME bsh, cgu: use ErrorHandler to handle unhandled promise rejections. Just replacing jQuery.Deferred.exceptionHook(error, stack) does not work
// because it is called on every exception and not only on unhandled.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event would be exactly what we need, but jQuery does not support it.
// Bluebird has a polyfill -> can it be ported to jQuery?
}
_createErrorHandler(opts) {
opts = jquery__WEBPACK_IMPORTED_MODULE_1___default().extend({}, opts);
return _index__WEBPACK_IMPORTED_MODULE_0__.scout.create(_index__WEBPACK_IMPORTED_MODULE_0__.ErrorHandler, opts);
}
/**
* Uses the object returned by {@link _ajaxDefaults} to set up ajax. The values in that object are used as default values for every ajax call.
*/
_ajaxSetup() {
let ajaxDefaults = this._ajaxDefaults();
if (ajaxDefaults) {
jquery__WEBPACK_IMPORTED_MODULE_1___default().ajaxSetup(ajaxDefaults);
}
}
/**
* Returns the defaults for every ajax call. You may override it to set custom defaults.
* By default {@link _beforeAjaxCall} is assigned to the beforeSend method.
*
* Note: This will affect every ajax call, so use it with care! See also the advice on https://api.jquery.com/jquery.ajaxsetup/.
*/
_ajaxDefaults() {
return {
beforeSend: this._beforeAjaxCall.bind(this)
};
}
/**
* Called before every ajax call. Sets the header X-Scout-Correlation-Id.
*
* Maybe overridden to set custom headers or to execute other code which should run before an ajax call.
*/
_beforeAjaxCall(request, settings) {
request.setRequestHeader('X-Scout-Correlation-Id', _index__WEBPACK_IMPORTED_MODULE_0__.numbers.correlationId());
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // explicitly add here because jQuery only adds it automatically if it is no crossDomain request
if (this.sessions[0] && this.sessions[0].ready) {
request.setRequestHeader('Accept-Language', this.sessions[0].locale.languageTag);
}
}
_loadSessions(options) {
options = options || {};
let promises = [];
jquery__WEBPACK_IMPORTED_MODULE_1___default()('.scout').each((i, elem) => {
let $entryPoint = jquery__WEBPACK_IMPORTED_MODULE_1___default()(elem);
options.portletPartId = options.portletPartId || $entryPoint.data('partid') || '0';
let promise = this._loadSession($entryPoint, options);
promises.push(promise);
});
return jquery__WEBPACK_IMPORTED_MODULE_1___default().promiseAll(promises);
}
/**
* @returns promise which is resolved when the session is ready
*/
_loadSession($entryPoint, model) {
let sessionModel = {
$entryPoint: $entryPoint
};
let options = jquery__WEBPACK_IMPORTED_MODULE_1___default().extend({}, model, sessionModel);
options.locale = options.locale || this._loadLocale();
let session = this._createSession(options);
this.sessions.push(session);
// TODO [7.0] cgu improve this, start must not be executed because it currently does a server request
let desktop = this._createDesktop(session.root);
this._triggerDesktopReady(desktop);
session.render(() => {
session._renderDesktop();
// Ensure layout is valid (explicitly layout immediately and don't wait for setTimeout to run to make layouting invisible to the user)
session.layoutValidator.validate();
session.focusManager.validateFocus();
session.ready = true;
this._triggerSessionReady(session);
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isInfoEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info('Session initialized. Detected ' + _index__WEBPACK_IMPORTED_MODULE_0__.Device.get());
});
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise();
}
/** @internal */
_triggerDesktopReady(desktop) {
this.trigger('desktopReady', {
desktop: desktop
});
}
/** @internal */
_triggerSessionReady(session) {
this.trigger('sessionReady', {
session: session
});
}
_createSession(options) {
return _index__WEBPACK_IMPORTED_MODULE_0__.scout.create(_index__WEBPACK_IMPORTED_MODULE_0__.Session, options);
}
_createDesktop(parent) {
return _index__WEBPACK_IMPORTED_MODULE_0__.scout.create(_index__WEBPACK_IMPORTED_MODULE_0__.Desktop, {
parent: parent
});
}
/**
* @returns the locale to be used when no locale is provided as session option. By default, the navigators locale is used.
*/
_loadLocale() {
return _index__WEBPACK_IMPORTED_MODULE_0__.locales.getNavigatorLocale();
}
_initDone(options) {
this.initialized = true;
this.setLoading(false);
this.trigger('init', {
options: options
});
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.isInfoEnabled() && jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info('App initialized');
}
_fail(options, error) {
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
args[_key - 2] = arguments[_key];
}
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.error('App initialization failed.');
this.setLoading(false);
let promises = [];
if (_index__WEBPACK_IMPORTED_MODULE_0__.webstorage.getItemFromSessionStorage('scout:bootstrapErrorPageReload')) {
// Do not append a message, page is about to be reloaded
} else if (this.sessions.length === 0) {
promises.push(this.errorHandler.handle(error, ...args).then(errorInfo => {
this._appendStartupError(jquery__WEBPACK_IMPORTED_MODULE_1___default()('body'), errorInfo);
}));
} else {
// Session.js may already display a fatal message box
// -> don't handle the error again and display multiple error messages
this.sessions.filter(session => !session.ready && !session.isFatalMessageShown()).forEach(session => {
session.$entryPoint.empty();
const errorHandler = this._createErrorHandler({
session: session
});
const promise = errorHandler.analyzeError(error).then(info => {
info.showAsFatalError = true;
return errorHandler.handleErrorInfo(info);
});
promises.push(promise);
});
}
this.trigger('fail', {
error
});
// Reject with original rejection arguments
return jquery__WEBPACK_IMPORTED_MODULE_1___default().promiseAll(promises).then(errorInfo => jquery__WEBPACK_IMPORTED_MODULE_1___default().rejectedPromise(error, ...args));
}
_appendStartupError($parent, errorInfo) {
let $error = $parent.appendDiv('startup-error');
$error.appendDiv('startup-error-title').text('The application could not be started');
let message = errorInfo.message;
if (errorInfo?.httpStatus) {
message = this.errorHandler.getMessageBodyForHttpStatus(errorInfo?.httpStatus);
}
if (message) {
$error.appendDiv('startup-error-message').text(message);
}
}
/**
* Override this method to install extensions to Scout objects. Since the extension feature replaces functions
* on the prototype of the Scout objects you must apply 'function patches' to Scout framework or other code before
* the extensions are installed.
*
* The default implementation does nothing.
*/
_installExtensions() {
// NOP
}
/**
* @see AppEventMap#installExtensions
*/
_triggerInstallExtensions() {
this.trigger('installExtensions');
}
}
/***/ }),
/***/ "./src/AppEventMap.ts":
/*!****************************!*\
!*** ./src/AppEventMap.ts ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/***/ }),
/***/ "./src/ErrorHandler.ts":
/*!*****************************!*\
!*** ./src/ErrorHandler.ts ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ ErrorHandler: () => (/* binding */ ErrorHandler)
/* harmony export */ });
/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index */ "./src/index.ts");
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! jquery */ "jquery");
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var sourcemapped_stacktrace__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! sourcemapped-stacktrace */ "sourcemapped-stacktrace");
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
class ErrorHandler {
objectType;
logError;
displayError;
sendError;
session;
windowErrorHandler;
constructor() {
this.logError = true;
this.displayError = true;
this.sendError = false;
this.windowErrorHandler = this._onWindowError.bind(this);
this.session = null;
}
/**
* Use this constant to configure whether all instances of the ErrorHandler should write
* to the console. When you've installed a console appender to log4javascript you can set the
* value to false, because the ErrorHandler also calls $.log.error and thus the appender has
* already written the message to the console. We don't want to see it twice.
*/
static CONSOLE_OUTPUT = true;
init(options) {
jquery__WEBPACK_IMPORTED_MODULE_1___default().extend(this, options);
}
// Signature matches the "window.onerror" event handler
// https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
_onWindowError(errorMessage, fileName, lineNumber, columnNumber, error) {
try {
if (this._isIgnorableScriptError(errorMessage, fileName, lineNumber, columnNumber, error)) {
this.handleErrorInfo({
log: `Ignoring error. Message: ${errorMessage}`,
showAsFatalError: true,
level: _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.INFO
});
return;
}
if (error instanceof Error) {
this.analyzeError(error).then(info => {
info.showAsFatalError = true;
return this.handleErrorInfo(info);
}).catch(error => console.error('Error in global JavaScript error handler', error));
return;
}
let code = 'J00';
let log = errorMessage + ' at ' + fileName + ':' + lineNumber + '\n(' + 'Code ' + code + ')';
this.handleErrorInfo({
message: errorMessage,
showAsFatalError: true,
code: code,
log: log
});
} catch (err) {
throw new Error('Error in global JavaScript error handler: ' + err.message + ' (original error: ' + errorMessage + ' at ' + fileName + ':' + lineNumber + ')');
}
}
_isIgnorableScriptError(message, fileName, lineNumber, columnNumber, error) {
// Ignore errors caused by scripts from a different origin.
// Example: Firefox on iOS throws an error, probably caused by an internal Firefox script.
// The error does not affect the application and cannot be prevented by the app either since we don't know what script it is and what it does.
// In that case the error must not be shown to the user, instead just log it silently.
// https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
return message && message.toLowerCase().indexOf('script error') > -1 && !fileName && !lineNumber && !columnNumber && !error;
}
/**
* Handles unexpected JavaScript errors. The arguments are first analyzed and then handled.
*
* This method may be called by passing the arguments individually or as an array (or array-like object)
* in the first argument.
* Examples:
* 1. try { ... } catch (err) { handler.handle(err); }
* 2. $.get().fail(function(jqXHR, textStatus, errorThrown) { handler.handle(jqXHR, textStatus, errorThrown); }
* 3. $.get().fail(function(jqXHR, textStatus, errorThrown) { handler.handle(arguments); } // <-- recommended
*
* @param errorOrArgs error or array or array-like object containing the error and other arguments
* @returns the analyzed errorInfo
*/
handle(errorOrArgs) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
let error = errorOrArgs;
if (errorOrArgs && args.length === 0) {
if (String(errorOrArgs) === '[object Arguments]') {
error = errorOrArgs[0];
args = [...errorOrArgs].slice(1);
} else if (Array.isArray(errorOrArgs)) {
error = errorOrArgs[0];
args = errorOrArgs.slice(1);
}
}
return this.analyzeError(error, ...args).then(this.handleErrorInfo.bind(this));
}
/**
* Returns an "errorInfo" object for the given arguments. The following cases are handled:
* 1. Error objects (code: computed by getJsErrorCode())
* 2. jQuery AJAX errors (code: 'X' + HTTP status code)
* 3. Nothing (code: 'P3')
* 4. Everything else (code: 'P4')
*/
analyzeError(error) {
let errorInfo = {
error: error,
message: null,
code: null,
httpStatus: null,
errorDo: null,
stack: null,
mappedStack: null,
mappingError: null,
log: null,
debugInfo: null
};
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
return this._analyzeError(errorInfo, ...args);
}
_analyzeError(errorInfo) {
let error = errorInfo.error;
// 1. Regular errors
if (error instanceof Error) {
// Map stack first before analyzing the error
return this.mapStack(error.stack).catch(result => {
errorInfo.mappingError = result.message + '\n' + result.error.message + '\n' + result.error.stack;
return null;
}).then(mappedStack => {
errorInfo.mappedStack = mappedStack;
this._analyzeRegularError(errorInfo);
return errorInfo;
});
}
// 2. Ajax errors
if (jquery__WEBPACK_IMPORTED_MODULE_1___default().isJqXHR(error) || Array.isArray(error) && jquery__WEBPACK_IMPORTED_MODULE_1___default().isJqXHR(error[0]) || error instanceof _index__WEBPACK_IMPORTED_MODULE_0__.AjaxError) {
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
this._analyzeAjaxError(errorInfo, ...args);
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise(errorInfo);
}
// 3. No reason provided
if (!error) {
this._analyzeNoError(errorInfo);
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise(errorInfo);
}
// 4. Everything else (e.g. when strings are thrown)
this._analyzeOtherError(errorInfo);
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise(errorInfo);
}
_analyzeRegularError(errorInfo) {
let error = errorInfo.error;
errorInfo.code = this.getJsErrorCode(error);
errorInfo.showAsFatalError = true;
errorInfo.message = String(error.message || error);
if (error.stack) {
errorInfo.stack = String(error.stack);
}
if (error['debugInfo']) {
// scout extension
errorInfo.debugInfo = error['debugInfo'];
}
let stack = errorInfo.mappedStack || errorInfo.stack;
let log = [];
if (!stack || stack.indexOf(errorInfo.message) === -1) {
// Only log message if not already included in stack
log.push(errorInfo.message);
}
if (stack) {
log.push(stack);
}
if (errorInfo.mappingError) {
log.push(errorInfo.mappingError);
}
if (errorInfo.debugInfo) {
// Error throwers may put a "debugInfo" string on the error object that is then added to the log string (this is a scout extension).
log.push('----- Additional debug information: -----\n' + errorInfo.debugInfo);
}
errorInfo.log = _index__WEBPACK_IMPORTED_MODULE_0__.arrays.format(log, '\n');
}
_analyzeAjaxError(errorInfo) {
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
args[_key4 - 1] = arguments[_key4];
}
let error = errorInfo.error;
let jqXHR,
errorThrown,
requestOptions,
errorDo = null;
if (error instanceof _index__WEBPACK_IMPORTED_MODULE_0__.AjaxError) {
// Scout Ajax Error
jqXHR = error.jqXHR;
errorThrown = error.errorThrown;
requestOptions = error.requestOptions; // scout extension
errorDo = error.errorDo;
} else {
// jQuery $.ajax() error (arguments of the fail handler are: jqXHR, textStatus, errorThrown, requestOptions)
// The first argument (jqXHR) is stored in errorInfo.error (may even be an array) -> create args array again and extract the parameters
args = _index__WEBPACK_IMPORTED_MODULE_0__.arrays.ensure(error).concat(args);
jqXHR = args[0];
errorThrown = args[2];
requestOptions = args[3]; // scout extension
let errorDoCandidate = jqXHR?.responseJSON?.error;
if (_index__WEBPACK_IMPORTED_MODULE_0__.AjaxError.isErrorDo(errorDoCandidate)) {
errorDo = errorDoCandidate;
}
}
let ajaxRequest = this.formatAjaxRequest(requestOptions);
let ajaxStatus = this.formatAjaxStatus(jqXHR, errorThrown);
errorInfo.httpStatus = jqXHR.status || 0;
errorInfo.code = 'X' + errorInfo.httpStatus;
errorInfo.errorDo = errorDo;
if (errorDo) {
errorInfo.message = errorDo.message;
errorInfo.level = this._severityToLogLevel(errorDo.severity);
}
// logging info
let req = 'AJAX call' + _index__WEBPACK_IMPORTED_MODULE_0__.strings.box(' "', ajaxRequest, '"') + ' failed' + _index__WEBPACK_IMPORTED_MODULE_0__.strings.box(' [', ajaxStatus, ']');
let log = [];
if (errorInfo.message) {
log.push(errorInfo.message);
} else {
errorInfo.message = req;
}
log.push(req);
if (jqXHR.responseText) {
errorInfo.debugInfo = 'Response text:\n' + jqXHR.responseText;
log.push(errorInfo.debugInfo);
}
errorInfo.log = _index__WEBPACK_IMPORTED_MODULE_0__.arrays.format(log, '\n');
}
formatAjaxStatus(jqXHR, errorThrown) {
return jqXHR.status ? _index__WEBPACK_IMPORTED_MODULE_0__.strings.join(' ', jqXHR.status + '', errorThrown) : 'Connection error';
}
formatAjaxRequest(requestOptions) {
return requestOptions ? _index__WEBPACK_IMPORTED_MODULE_0__.strings.join(' ', requestOptions.method, requestOptions.url) : '';
}
_severityToLogLevel(severity) {
switch (severity) {
case 'ok':
return _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.DEBUG;
case 'info':
return _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.INFO;
case 'warning':
return _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.WARN;
default:
return _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.ERROR;
}
}
_analyzeOtherError(errorInfo) {
let error = errorInfo.error;
// Everything else (e.g. when strings are thrown)
let s = typeof error === 'string' || typeof error === 'number' ? String(error) : null;
errorInfo.code = 'P4';
errorInfo.message = s || 'Unexpected error';
if (!s) {
try {
s = JSON.stringify(error); // may throw "cyclic object value" error
} catch (err) {
s = String(error);
}
}
errorInfo.log = 'Unexpected error: ' + s;
}
_analyzeNoError(errorInfo) {
errorInfo.code = 'P3';
errorInfo.message = 'Unknown error';
errorInfo.log = 'Unexpected error (no reason provided)';
}
mapStack(stack) {
let deferred = jquery__WEBPACK_IMPORTED_MODULE_1___default().Deferred();
try {
sourcemapped_stacktrace__WEBPACK_IMPORTED_MODULE_2__.mapStackTrace(stack, mappedStack => {
deferred.resolve(_index__WEBPACK_IMPORTED_MODULE_0__.arrays.format(mappedStack, '\n'));
});
} catch (e) {
return jquery__WEBPACK_IMPORTED_MODULE_1___default().rejectedPromise({
message: 'stacktrace mapping failed',
error: e
});
}
return deferred.promise();
}
/**
* Expects an object as returned by {@link analyzeError} and handles it:
* - If the flag "logError" is set, the log message is printed to the console
* - If there is a scout session and the flag "displayError" is set, the error is shown in a message box.
* - If there is a scout session and the flag "sendError" is set, the error is sent to the UI server.
*/
handleErrorInfo(errorInfo) {
errorInfo.level = _index__WEBPACK_IMPORTED_MODULE_0__.scout.nvl(errorInfo.level, _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.ERROR);
if (this.logError && errorInfo.log) {
this._logErrorInfo(errorInfo);
}
// Note: The error handler is installed globally, and we cannot tell in which scout session the error happened.
// We simply use the first scout session to display the message box and log the error. This is not ideal in the
// multi-session-case (portlet), but currently there is no other way. Besides, this feature is not in use yet.
let session = this.session || _index__WEBPACK_IMPORTED_MODULE_0__.App.get().sessions[0];
if (session) {
if (this.sendError) {
this._sendErrorMessage(session, errorInfo.log, errorInfo.level);
}
if (this.displayError) {
if (errorInfo.showAsFatalError) {
if (errorInfo.level === _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.ERROR) {
return this._showInternalUiErrorMessageBox(session, errorInfo.message, errorInfo.code, errorInfo.log).then(() => errorInfo);
}
} else {
return this._showErrorMessageBox(session, errorInfo).then(() => errorInfo);
}
}
}
return jquery__WEBPACK_IMPORTED_MODULE_1___default().resolvedPromise(errorInfo);
}
_logErrorInfo(errorInfo) {
switch (errorInfo.level) {
case _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.TRACE:
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.trace(errorInfo.log);
break;
case _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.DEBUG:
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.debug(errorInfo.log);
break;
case _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.INFO:
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.info(errorInfo.log);
break;
case _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.WARN:
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.warn(errorInfo.log);
break;
default:
jquery__WEBPACK_IMPORTED_MODULE_1___default().log.error(errorInfo.log);
}
// Note: when the null-logger is active it has already written the error to the console
// when the $.log.error function has been called above, so we don't have to log again here.
let writeToConsole = ErrorHandler.CONSOLE_OUTPUT;
if ((jquery__WEBPACK_IMPORTED_MODULE_1___default().log) instanceof _index__WEBPACK_IMPORTED_MODULE_0__.NullLogger) {
writeToConsole = false;
}
if (writeToConsole && window && window.console) {
if (errorInfo.level === _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.ERROR && window.console.error) {
window.console.error(errorInfo.log);
} else if (errorInfo.level === _index__WEBPACK_IMPORTED_MODULE_0__.LogLevel.WARN && window.console.warn) {
window.console.warn(errorInfo.log);
} else if (window.console.log) {
window.console.log(errorInfo.log);
}
}
}
/**
* Generate a "cool looking" error code from the JS error object, that
* does not reveal too much technical information, but at least indicates
* that a JS runtime error has occurred. (In contrast, fatal errors from
* the server have numeric error codes.)
*/
getJsErrorCode(error) {
if (error) {
if (error.name === 'EvalError') {
return 'E1';
}
if (error.name === 'InternalError') {
return 'I2';
}
if (error.name === 'RangeError') {
return 'A3';
}
if (error.name === 'ReferenceError') {
return 'R4';
}
if (error.name === 'SyntaxError') {
return 'S5';
}
if (error.name === 'TypeError') {
return 'T6';
}
if (error.name === 'URIError') {
return 'U7';
}
}
return 'J0';
}
_showErrorMessageBox(session, errorInfo) {
const parent = session.desktop || new _index__WEBPACK_IMPORTED_MODULE_0__.NullWidget();
const msgBoxModel = {
parent,
session,
...this._buildErrorMessageBoxModel(session, errorInfo)
};
const messageBox = _index__WEBPACK_IMPORTED_MODULE_0__.scout.create(_index__WEBPACK_IMPORTED_MODULE_0__.MessageBox, msgBoxModel);
messageBox.on('action', event => messageBox.close());
messageBox.render(session.$entryPoint); // do not use messageBox.open() as the Desktop might not yet be ready (e.g. if there is an error in the App startup)
return messageBox.when('action');
}
_buildErrorMessageBoxModel(session, errorInfo) {
const status = this.errorInfoToStatus(session, errorInfo);
let code = null;
if (errorInfo.error instanceof _index__WEBPACK_IMPORTED_MODULE_0__.Status && errorInfo.error.code !== 0) {
code = errorInfo.error.code + '';
}
if (errorInfo?.errorDo?.errorCode && errorInfo.errorDo.errorCode !== '0') {
code = errorInfo.errorDo.errorCode;
}
let body = status.message || session.optText('ui.UnexpectedProblem', 'Unexpected problem');
if (code) {
body += ' (' + session.optText('ui.ErrorCodeX', 'Code ' + code, code) + ').';
}
let html = null;
if (errorInfo?.errorDo?.correlationId) {
let $container = session.desktop?.$container || session.$entryPoint;
let correlationIdText = session.optText('CorrelationId', 'Correlation ID');
html = $container.makeDiv('error-popup-correlation-id', correlationIdText + ': ' + errorInfo.errorDo.correlationId)[0].outerHTML;
}
return {
header: errorInfo.errorDo?.title,
body: body,
html: html,
iconId: status.iconId,
severity: status.severity,
hiddenText: errorInfo.log,
yesButtonText: session.optText('Ok', 'Ok')
};
}
_showInternalUiErrorMessageBox(session, errorMessage, errorCode, logMessage) {
let options = {
header: session.optText('ui.UnexpectedProblem', 'Internal UI Error'),
body: _index__WEBPACK_IMPORTED_MODULE_0__.strings.join('\n\n', session.optText('ui.InternalUiErrorMsg', errorMessage, ' (' + session.optText('ui.ErrorCodeX', 'Code ' + errorCode, errorCode) + ')'), session.optText('ui.UiInconsistentMsg', '')),
yesButtonText: session.optText('ui.Reload', 'Reload'),
yesButtonAction: _index__WEBPACK_IMPORTED_MODULE_0__.scout.reloadPage,
noButtonText: undefined,
hiddenText: logMessage,
iconId: _index__WEBPACK_IMPORTED_MODULE_0__.icons.SLIPPERY
};
if (session.inDevelopmentMode) {
options.noButtonText = session.optText('ui.Ignore', 'Ignore');
}
return session.showFatalMessage(options, errorCode);
}
_sendErrorMessage(session, logMessage, logLevel) {
session.sendLogRequest(logMessage, logLevel);
}
errorInfoToStatus(session, errorInfo) {
if (errorInfo.error instanceof _index__WEBPACK_IMPORTED_MODULE_0__.Status) {
// if a Status is thrown
return errorInfo.error;
}
return _index__WEBPACK_IMPORTED_MODULE_0__.Status.ensure({
message: this._errorInfoToStatusMessage(session, errorInfo),
severity: this._errorInfoToStatusSeverity(errorInfo),
code: this._errorInfoToStatusCode(errorInfo)
});
}
_errorInfoToStatusCode(errorInfo) {
let errorCode = errorInfo?.errorDo?.errorCode;
if (errorCode && errorCode !== '0' && _index__WEBPACK_IMPORTED_MODULE_0__.objects.isNumber(errorCode)) {
return _index__WEBPACK_IMPORTED_MODULE_0__.numbers.ensure(errorCode);
}
return errorInfo.httpStatus;
}
_errorInfoToStatusSeverity(errorInfo) {
switch (errorInfo?.errorDo?.severity) {
case 'ok':
return _index__WEBPACK_IMPORTED_MODULE_0__.Status.Severity.OK;
case 'info':
return _index__WEBPACK_IMPORTED_MODULE_0__.Status.Severity.INFO;
case 'warning':
return _index__WEBPACK_IMPORTED_MODULE_0__.Status.Severity.WARNING;
default:
return _index__WEBPACK_IMPORTED_MODULE_0__.Status.Severity.ERROR;
}
}
_errorInfoToStatusMessage(session, errorInfo) {
if (typeof errorInfo.error === 'string') {
return errorInfo.error;
}
const errorDo = errorInfo.errorDo;
if (errorDo) {
let body = errorDo.message;
if (body && body !== 'undefined') {
// ProcessingException has default text 'undefined' which is not very helpful -> don't use it
return body;
}
}
if (errorInfo.error?.message) {
return errorInfo.error.message;
}
if (errorInfo.message) {
return errorInfo.message;
}
return this.getMessageBodyForHttpStatus(errorInfo?.httpStatus, session);
}
/**
* Gets the default error message body for the given HTTP status error code.
* @param httpStatus The HTTP status error code. E.g. 503 (Service Unavailable)
* @param session An optional Session for a message in the language of the user. The default language is used if omitted.
* @returns The error message for the given error HTTP status.
*/
getMessageBodyForHttpStatus(httpStatus, session) {
let body;
let textMap = session ? session.textMap : _index__WEBPACK_IMPORTED_MODULE_0__.texts.get('default');
switch (httpStatus) {
case 404:
// Not Found
body = textMap.optGet('TheRequestedResourceCouldNotBeFound', 'The requested resource could not be found.');
break;
case 502: // Bad Gateway
case 503: // Service Unavailable
case 504:
// Gateway Timeout
body = textMap.optGet('NetSystemsNotAvailable', 'The system is partially unavailable at the moment.') + '\n\n' + textMap.optGet('PleaseTryAgainLater', 'Please try again later.');
break;
case 403:
// Forbidden
body = textMap.optGet('YouAreNotAuthorizedToPerformThisAction', 'You are not authorized to perform this action.');
break;
default:
body = textMap.optGet('ui.UnexpectedProblem', 'Unexpected problem');
}
return body;
}
}
/***/ }),
/***/ "./src/Extension.ts":
/*!**************************!*\
!*** ./src/Extension.ts ***!
\**************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Extension: () => (/* binding */ Extension)
/* harmony export */ });
/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index */ "./src/index.ts");
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
/**
* This class is used to extend an existing Scout object. In or