blob: 8a7cf3dd0d400b60cc12f1203b501e0763552c31 [file] [log] [blame]
/*
* Copyright (c) 2010-2019 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {codes, Device, EventSupport, fonts, locales, logging, numbers, ObjectFactory, objects, polyfills, scout, Session, texts, webstorage} from './index';
import * as $ from 'jquery';
let instance = null;
let listeners = [];
export default class App {
static addListener(type, func) {
var listener = {
type: type,
func: func
};
if (instance) {
instance.events.addListener(listener);
} else {
listeners.push(listener);
}
return listener;
}
static get() {
return instance;
}
constructor() {
this.events = this._createEventSupport();
this.initialized = false;
this.sessions = [];
// register the listeners which were added to scout before the app is created
listeners.forEach(function(listener) {
this.addListener(listener);
}, this);
listeners = [];
instance = this;
this.errorHandler = this._createErrorHandler();
}
/**
* Main initialization function.<p>
*
* Calls this._prepare, this._bootstrap and this._init.<p>
* 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, polyfills 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(function() {
this._prepareEssentials(options);
this._prepareDone(options);
}.bind(this));
}
_prepareEssentials(options) {
polyfills.install(window);
ObjectFactory.get().init();
}
_prepareDone(options) {
this.trigger('prepare', {
options: options
});
$.log.isDebugEnabled() && $.log.debug('App prepared');
}
_prepareLogging(options) {
return logging.bootstrap();
}
/**
* Executes the default bootstrap functions and returns an array of promises.<p>
* The actual session startup begins only when every of these promises are completed.
* This gives the possibility to dynamically load additional scripts or files which are mandatory for a successful session startup.
* The individual bootstrap functions may return null or undefined, a single promise or multiple promises as an array.
*/
_bootstrap(options) {
options = options || {};
var promises = [];
this._doBootstrap(options).forEach(function(value) {
if (Array.isArray(value)) {
promises.concat(value);
} else if (value) {
promises.push(value);
}
});
return $.promiseAll(promises)
.done(this._bootstrapDone.bind(this, options))
.catch(this._bootstrapFail.bind(this, options));
}
_doBootstrap(options) {
return [
Device.get().bootstrap(),
fonts.bootstrap(options.fonts),
locales.bootstrap(options.localesUrl),
texts.bootstrap(options.textsUrl),
codes.bootstrap(options.codesUrl)
];
}
_bootstrapDone(options) {
webstorage.removeItem(sessionStorage, 'scout:timeoutPageReload');
this.trigger('bootstrap', {
options: options
});
$.log.isDebugEnabled() && $.log.debug('App bootstrapped');
}
_bootstrapFail(options, vararg, textStatus, errorThrown, requestOptions) {
$.log.isInfoEnabled() && $.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 return a session timeout 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.
if ($.isJqXHR(vararg)) {
// Ajax error
// If a resource returns 401 (unauthorized) it is likely a session timeout.
// This may happen if no Scout backend is used or a reverse proxy returned the response, otherwise status 200 with an error object would be returned, see below
if (this._isSessionTimeoutStatus(vararg.status)) {
var url = requestOptions ? requestOptions.url : '';
this._handleBootstrapTimeoutError(vararg, url);
return;
}
} else if (objects.isPlainObject(vararg) && vararg.error) {
// Json based error
// Json errors (normally processed by Session.js) are returned with http status 200
if (vararg.error.code === Session.JsonResponseError.SESSION_TIMEOUT) {
this._handleBootstrapTimeoutError(vararg.error, vararg.url);
return;
}
}
// Make sure promise will be rejected with all original arguments so that it can be eventually handled by this._fail
var args = objects.argumentsToArray(arguments).slice(1);
return $.rejectedPromise.apply($, args);
}
_isSessionTimeoutStatus(httpStatus) {
return httpStatus === 401;
}
_handleBootstrapTimeoutError(error, url) {
$.log.isInfoEnabled() && $.log.info('Timeout error for resource ' + url + '. Reloading page...');
if (webstorage.getItem(sessionStorage, 'scout:timeoutPageReload')) {
// Prevent loop in case a reload did not solve the problem
$.log.isWarnEnabled() && $.log.warn('Prevented automatic reload, startup will likely fail', error, url);
webstorage.removeItem(sessionStorage, 'scout:timeoutPageReload');
throw new Error('Resource ' + url + ' could not be loaded due to a session timeout, even after a page reload');
}
webstorage.setItem(sessionStorage, 'scout:timeoutPageReload', true);
// See comment in _bootstrapFail for the reasons why to reload here
scout.reloadPage();
}
/**
* Initializes a session for each html element with class '.scout' and stores them in scout.sessions.
*/
_init(options) {
options = options || {};
if (!this._checkBrowserCompability(options)) {
return;
}
this._initVersion(options);
this._prepareDOM();
this._installErrorHandler();
this._installGlobalMouseDownInterceptor();
this._installSyntheticActiveStateHandler();
this._ajaxSetup();
this._installExtensions();
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} promise which is resolved after the loading is complete
*/
_load(options) {
return $.resolvedPromise();
}
_checkBrowserCompability(options) {
var device = Device.get();
var app = this;
$.log.isInfoEnabled() && $.log.info('Detected browser ' + device.browser + ' version ' + device.browserVersion);
if (!scout.nvl(options.checkBrowserCompatibility, true) || device.isSupportedBrowser()) {
// No check requested or browser is supported
return true;
}
$('.scout').each(function() {
var $entryPoint = $(this),
$box = $entryPoint.appendDiv(),
newOptions = objects.valueCopy(options);
newOptions.checkBrowserCompatibility = false;
$box.load('unsupported-browser.html', function() {
$box.find('button').on('click', function() {
$box.remove();
app._init(newOptions);
});
});
});
return false;
}
_initVersion(options) {
this.version = scout.nvl(
this.version,
options.version,
$('scout-version').data('value'));
}
_prepareDOM() {
scout.prepareDOM(document);
}
_installGlobalMouseDownInterceptor() {
scout.installGlobalMouseDownInterceptor(document);
}
_installSyntheticActiveStateHandler() {
scout.installSyntheticActiveStateHandler(document);
}
/**
* Installs a global error handler.
* <p>
* 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.
* <p>
* 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
// --> replace jQuery.Deferred.exceptionHook(error, stack)
}
_createErrorHandler() {
return scout.create('ErrorHandler');
}
/**
* Uses the object returned by {@link #_ajaxDefaults} to setup ajax. The values in that object are used as default values for every ajax call.
*/
_ajaxSetup() {
var ajaxDefaults = this._ajaxDefaults();
if (ajaxDefaults) {
$.ajaxSetup(ajaxDefaults);
}
}
/**
* Returns the defaults for every ajax call. You may override it to set custom defaults.
* By default _beforeAjaxCall is assigned to the beforeSend method.
* <p>
* 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.
* <p>
* Maybe overridden to set custom headers or to execute other code which should run before an ajax call.
*/
_beforeAjaxCall(request) {
request.setRequestHeader('X-Scout-Correlation-Id', numbers.correlationId());
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // explicitly add here because jQuery only adds it automatically if it is no crossDomain request
}
_loadSessions(options) {
options = options || {};
var promises = [];
$('.scout').each(function(i, elem) {
var $entryPoint = $(elem);
options.portletPartId = options.portletPartId || $entryPoint.data('partid') || '0';
var promise = this._loadSession($entryPoint, options);
promises.push(promise);
}.bind(this));
return $.promiseAll(promises);
}
/**
* @returns {Promise} promise which is resolved when the session is ready
*/
_loadSession($entryPoint, options) {
options.locale = options.locale || this._loadLocale();
options.$entryPoint = $entryPoint;
var 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
var desktop = this._createDesktop(session.root);
this.trigger('desktopReady', {
desktop: desktop
});
session.render(function() {
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.trigger('sessionReady', {
session: session
});
$.log.isInfoEnabled() && $.log.info('Session initialized. Detected ' + Device.get());
}.bind(this));
return $.resolvedPromise();
}
_createSession(options) {
return scout.create('Session', options, {
ensureUniqueId: false
});
}
_createDesktop(parent) {
return scout.create('Desktop', {
parent: parent
});
}
/**
* @returns the locale to be used when no locale is provided as app option. By default the navigators locale is used.
*/
_loadLocale() {
return locales.getNavigatorLocale();
}
_initDone(options) {
this.initialized = true;
this.trigger('init', {
options: options
});
$.log.isInfoEnabled() && $.log.info('App initialized');
}
_fail(options, error) {
$.log.error('App initialization failed', error);
var $error = $('body').appendDiv('startup-error');
$error.appendDiv('startup-error-title').text('The application could not be started');
var args = objects.argumentsToArray(arguments).slice(1);
var errorInfo = this.errorHandler.handle(args);
if (errorInfo.message) {
$error.appendDiv('startup-error-message').text(errorInfo.message);
}
// Reject with original rejection arguments
return $.rejectedPromise.apply($, args);
}
/**
* 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
}
// --- Event handling methods ---
_createEventSupport() {
return new EventSupport();
}
trigger(type, event) {
event = event || {};
event.source = this;
this.events.trigger(type, event);
}
one(type, func) {
this.events.one(type, func);
}
on(type, func) {
return this.events.on(type, func);
}
off(type, func) {
this.events.off(type, func);
}
addListener(listener) {
this.events.addListener(listener);
}
removeListener(listener) {
this.events.removeListener(listener);
}
when(type) {
return this.events.when(type);
}
}
/*
* Copyright (c) 2010-2019 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/