| /* |
| * Copyright (c) 2014-2018 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 { |
| AjaxCall, |
| App, |
| arrays, |
| BackgroundJobPollingStatus, |
| BackgroundJobPollingSupport, |
| Device, |
| EventSupport, |
| FileInput, |
| FocusManager, |
| fonts, |
| LayoutValidator, |
| Locale, |
| MessageBox, |
| ModelAdapter, |
| NullWidget, |
| ObjectFactory, |
| objects, |
| Reconnector, |
| RemoteEvent, |
| ResponseQueue, |
| scout, |
| Status, |
| strings, |
| TextMap, |
| texts, |
| TypeDescriptor, |
| URL, |
| UserAgent, |
| webstorage |
| } from '../index'; |
| import * as $ from 'jquery'; |
| |
| export default class Session { |
| constructor() { |
| this.$entryPoint = null; |
| this.partId = 0; |
| |
| this.url = new URL(); |
| this.userAgent = new UserAgent({ |
| deviceType: Device.get().type, |
| touch: Device.get().supportsOnlyTouch(), |
| standalone: Device.get().isStandalone() |
| }); |
| this.locale = null; |
| this.textMap = new TextMap(); |
| |
| this.ready = false; // true after desktop has been completely rendered |
| this.unloading = false; // true when 'beforeOnload' event has been triggered |
| this.unloaded = false; // true after unload event has been received from the window |
| this.loggedOut = false; |
| this.inspector = false; |
| this.persistent = false; |
| this.desktop = null; |
| this.layoutValidator = new LayoutValidator(); |
| this.focusManager = null; |
| this.keyStrokeManager = null; |
| |
| this.uiSessionId = null; // assigned by server on session startup (OWASP recommendation, see https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern). |
| this.clientSessionId = this._getClientSessionIdFromStorage(); |
| this.forceNewClientSession = false; |
| this.remoteUrl = 'json'; |
| this.unloadUrl = 'unload'; |
| this.modelAdapterRegistry = {}; |
| this.ajaxCalls = []; |
| this.asyncEvents = []; |
| this.responseQueue = new ResponseQueue(this); |
| this.requestsPendingCounter = 0; |
| this.suppressErrors = false; |
| this.requestTimeoutCancel = 5000; // ms |
| this.requestTimeoutPoll = 75000; // ms, depends on polling interval, will therefore be initialized on startup |
| this.requestTimeoutPing = 5000; // ms |
| this.backgroundJobPollingSupport = new BackgroundJobPollingSupport(true); |
| this.reconnector = new Reconnector(this); |
| this.processingEvents = false; |
| |
| // This property is enabled by URL parameter &adapterExportEnabled=1. Default is false |
| this.adapterExportEnabled = false; |
| this._adapterDataCache = {}; |
| this._busy = false; |
| this._busyIndicator = null; |
| this._busyIndicatorTimeoutId = null; |
| this._deferred = null; |
| this._fatalMessagesOnScreen = {}; |
| this._retryRequest = null; |
| this._queuedRequest = null; |
| this.requestSequenceNo = 0; |
| |
| this.rootAdapter = new ModelAdapter(); |
| this.rootAdapter.init({ |
| session: this, |
| id: '1', |
| objectType: 'RootAdapter' |
| }); |
| |
| var rootParent = new NullWidget(); |
| rootParent.session = this; |
| rootParent.initialized = true; |
| |
| this.root = this.rootAdapter.createWidget({ |
| session: this, |
| id: '1', |
| objectType: 'NullWidget' |
| }, rootParent); |
| this.events = this._createEventSupport(); |
| } |
| |
| // Corresponds to constants in JsonResponse |
| static JsonResponseError = { |
| STARTUP_FAILED: 5, |
| SESSION_TIMEOUT: 10, |
| UI_PROCESSING: 20, |
| UNSAFE_UPLOAD: 30, |
| VERSION_MISMATCH: 40 |
| }; |
| |
| // Placeholder string for an empty filename |
| static EMPTY_UPLOAD_FILENAME = '*empty*'; |
| |
| /** |
| * $entryPoint is required to create a new session. |
| * |
| * The 'options' argument holds all optional values that may be used during |
| * initialization (it is the same object passed to the scout.init() function). |
| * The following 'options' properties are read by this constructor function: |
| * [portletPartId] |
| * Optional, default is 0. Necessary when multiple UI sessions are managed |
| * by the same window (portlet support). Each session's partId must be unique. |
| * [clientSessionId] |
| * Identifies the 'client instance' on the UI server. If the property is not set |
| * (which is the default case), the clientSessionId is taken from the browser's |
| * session storage (per browser window, survives F5 refresh of page). If no |
| * clientSessionId can be found, a new one is generated on the server. |
| * [userAgent] |
| * Default: DESKTOP |
| * [backgroundJobPollingEnabled] |
| * Unless websockets is used, this property turns on (default) or off background |
| * polling using an async ajax call together with setTimeout() |
| * [suppressErrors] |
| * Basically added because of Jasmine-tests. When working with async tests that |
| * use setTimeout(), sometimes the Jasmine-Maven plug-in fails and aborts the |
| * build because there were console errors. These errors always happen in this |
| * class. That's why we can skip suppress error handling with this flag. |
| * [focusManagerActive] |
| * Forces the focus manager to be active or not. If undefined, the value is |
| * auto detected by Device.js. |
| * [reconnectorOptions] |
| * Optional, properties of this object are copied to the Session's reconnector |
| * instance (see Reconnector.js). |
| * [ajaxCallOptions] |
| * Optional, properties of this object are copied to all instances of AjaxCall.js. |
| */ |
| init(model) { |
| var options = model || {}; |
| |
| if (!options.$entryPoint) { |
| throw new Error('$entryPoint is not defined'); |
| } |
| this.$entryPoint = options.$entryPoint; |
| this.partId = scout.nvl(options.portletPartId, this.partId); |
| this.forceNewClientSession = scout.nvl(this.url.getParameter('forceNewClientSession'), options.forceNewClientSession); |
| if (this.forceNewClientSession) { |
| this.clientSessionId = null; |
| } else { |
| this.clientSessionId = scout.nvl(options.clientSessionId, this.clientSessionId); |
| } |
| this.userAgent = scout.nvl(options.userAgent, this.userAgent); |
| this.suppressErrors = scout.nvl(options.suppressErrors, this.suppressErrors); |
| if (options.locale) { |
| this.locale = Locale.ensure(options.locale); |
| this.textMap = texts.get(this.locale.languageTag); |
| } |
| if (options.backgroundJobPollingEnabled === false) { |
| this.backgroundJobPollingSupport.enabled = false; |
| } |
| $.extend(this.reconnector, options.reconnectorOptions); |
| this.ajaxCallOptions = options.ajaxCallOptions; |
| |
| // Set inspector flag by looking at URL params. This is required when running in offline mode. |
| // In online mode, the server may override this flag again, see _processStartupResponse(). |
| if (this.url.getParameter('debug') === 'true' || this.url.getParameter('inspector') === 'true') { |
| this.inspector = true; |
| } |
| |
| if (this.url.getParameter('adapterExportEnabled') === 'true') { |
| this.adapterExportEnabled = true; |
| } |
| |
| // Install focus management for this session (cannot be created in constructor, because this.$entryPoint is required) |
| this.focusManager = new FocusManager({ |
| session: this, |
| active: options.focusManagerActive |
| }); |
| this.keyStrokeManager = scout.create('KeyStrokeManager', { |
| session: this |
| }); |
| } |
| |
| _throwError(message) { |
| if (!this.suppressErrors) { |
| throw new Error(message); |
| } |
| } |
| |
| unregisterModelAdapter(modelAdapter) { |
| delete this.modelAdapterRegistry[modelAdapter.id]; |
| } |
| |
| registerModelAdapter(modelAdapter) { |
| if (modelAdapter.id === undefined) { |
| throw new Error('modelAdapter.id must be defined'); |
| } |
| this.modelAdapterRegistry[modelAdapter.id] = modelAdapter; |
| } |
| |
| getModelAdapter(id) { |
| return this.modelAdapterRegistry[id]; |
| } |
| |
| getWidget(adapterId) { |
| if (!adapterId) { |
| return null; |
| } |
| if (typeof adapterId !== 'string') { |
| throw new Error('typeof adapterId must be string'); |
| } |
| var adapter = this.getModelAdapter(adapterId); |
| if (!adapter) { |
| return null; |
| } |
| return adapter.widget; |
| } |
| |
| getOrCreateWidget(adapterId, parent, strict) { |
| if (!adapterId) { |
| return null; |
| } |
| if (typeof adapterId !== 'string') { |
| throw new Error('typeof adapterId must be string'); |
| } |
| var widget = this.getWidget(adapterId); |
| if (widget) { |
| return widget; |
| } |
| var adapterData = this._getAdapterData(adapterId); |
| if (!adapterData) { |
| if (scout.nvl(strict, true)) { |
| throw new Error('no adapterData found for adapterId=' + adapterId); |
| } |
| return null; |
| } |
| var adapter = this.createModelAdapter(adapterData); |
| return adapter.createWidget(adapterData, parent); |
| } |
| |
| createModelAdapter(adapterData) { |
| var objectType = adapterData.objectType; |
| var createOpts = {}; |
| |
| var objectInfo = TypeDescriptor.parse(objectType); |
| if (objectInfo.modelVariant) { |
| objectType = objectInfo.objectType.toString() + 'Adapter' + ObjectFactory.MODEL_VARIANT_SEPARATOR + objectInfo.modelVariant.toString(); |
| // If no adapter exists for the given variant then create an adapter without variant. |
| // Mostly variant is only essential for the widget, not the adapter |
| createOpts.variantLenient = true; |
| } else { |
| objectType = objectInfo.objectType.toString() + 'Adapter'; |
| } |
| |
| // TODO [7.0] bsh, cgu: Add classId/modelClass? Think about if IDs should be different for widgets (maybe prefix with 'w') |
| var adapterModel = { |
| id: adapterData.id, |
| session: this |
| }; |
| var adapter = scout.create(objectType, adapterModel, createOpts); |
| $.log.isTraceEnabled() && $.log.trace('created new adapter ' + adapter); |
| return adapter; |
| } |
| |
| /** |
| * Sends the request asynchronously and processes the response later.<br> |
| * Furthermore, the request is sent delayed. If send is called multiple times |
| * during the same user interaction, the events are collected and sent in one |
| * request at the end of the user interaction |
| */ |
| sendEvent(event, delay) { |
| delay = delay || 0; |
| |
| this.asyncEvents = this._coalesceEvents(this.asyncEvents, event); |
| this.asyncEvents.push(event); |
| // Use the specified delay, except another event is already scheduled. In that case, use the minimal delay. |
| // This ensures that an event with a long delay doesn't hold back another event with a short delay. |
| this._asyncDelay = Math.min(delay, scout.nvl(this._asyncDelay, delay)); |
| |
| clearTimeout(this._sendTimeoutId); |
| this._sendTimeoutId = setTimeout(function() { |
| this._sendTimeoutId = null; |
| this._asyncDelay = null; |
| if (this.areRequestsPending()) { |
| // do not send if there are any requests pending because the order matters -> prevents race conditions |
| return; |
| } |
| this._sendNow(); |
| }.bind(this), this._asyncDelay); |
| } |
| |
| _sendStartupRequest() { |
| // Build startup request (see JavaDoc for JsonStartupRequest.java for details) |
| var request = this._newRequest({ |
| startup: true |
| }); |
| if (this.partId) { |
| request.partId = this.partId; |
| } |
| if (this.clientSessionId) { |
| request.clientSessionId = this.clientSessionId; |
| } |
| if (App.get().version) { |
| request.version = App.get().version; |
| } |
| request.userAgent = this.userAgent; |
| request.sessionStartupParams = this._createSessionStartupParams(); |
| |
| // Send request |
| var ajaxOptions = this.defaultAjaxOptions(request); |
| |
| return $.ajax(ajaxOptions) |
| .catch(onAjaxFail.bind(this)) |
| .then(onAjaxDone.bind(this)); |
| |
| // ----- Helper methods ----- |
| |
| function onAjaxDone(data) { |
| this._processStartupResponse(data); |
| if (data.error) { |
| return $.rejectedPromise(data); |
| } |
| } |
| |
| function onAjaxFail(jqXHR, textStatus, errorThrown) { |
| this._setApplicationLoading(false); |
| this._processErrorResponse(jqXHR, textStatus, errorThrown, request); |
| var args = objects.argumentsToArray(arguments); |
| return $.rejectedPromise.apply($, args); |
| } |
| } |
| |
| /** |
| * Creates an object to send to the server as "startupParams". |
| * |
| * Default params: |
| * "url": |
| * browser URL (without query and hash part) |
| * "geolocationServiceAvailable": |
| * true if browser supports geo location services |
| * |
| * Additionally, all query parameters from the URL are put in the resulting object. |
| */ |
| _createSessionStartupParams() { |
| var params = { |
| url: this.url.baseUrlRaw, |
| geolocationServiceAvailable: Device.get().supportsGeolocation() |
| }; |
| |
| // Extract query parameters from URL and put them in the resulting object |
| var urlParameterMap = this.url.parameterMap; |
| for (var prop in urlParameterMap) { |
| params[prop] = urlParameterMap[prop]; |
| } |
| return params; |
| } |
| |
| _processStartupResponse(data) { |
| // Handle errors from server |
| if (data.error) { |
| this._processErrorJsonResponse(data.error); |
| return; |
| } |
| |
| webstorage.removeItem(sessionStorage, 'scout:versionMismatch'); |
| |
| if (!data.startupData) { |
| throw new Error('Missing startupData'); |
| } |
| |
| // Mark session as persistent (means a persistent session cookie is used and the client session will be restored after a browser restart) |
| this.persistent = data.startupData.persistent; |
| |
| // Store clientSessionId in sessionStorage (to send the same ID again on page reload) |
| this._storeClientSessionIdInStorage(data.startupData.clientSessionId); |
| |
| // Assign server generated uiSessionId. It must be sent along with all further requests. |
| this.uiSessionId = data.startupData.uiSessionId; |
| |
| // Destroy UI session on server when page is closed or reloaded |
| $(window) |
| .on('beforeunload.' + this.uiSessionId, this._onWindowBeforeUnload.bind(this)) |
| .on('unload.' + this.uiSessionId, this._onWindowUnload.bind(this)); |
| |
| // Special case: Page must be reloaded on startup (e.g. theme changed) |
| if (data.startupData.reloadPage) { |
| scout.reloadPage(); |
| return; |
| } |
| |
| // Enable inspector mode if server requests it (e.g. when server is running in development mode) |
| if (data.startupData.inspector) { |
| this.inspector = true; |
| } |
| |
| // Init request timeout for poller |
| this.requestTimeoutPoll = (data.startupData.pollingInterval + 15) * 1000; |
| |
| // Register UI session |
| this.modelAdapterRegistry[this.uiSessionId] = this; // TODO [7.0] cgu: maybe better separate session object from event processing, create ClientSession.js?. If yes, desktop should not have rootadapter as parent, see 406 |
| |
| // Store adapters to adapter data cache |
| if (data.adapterData) { |
| this._copyAdapterData(data.adapterData); |
| } |
| |
| this.locale = new Locale(data.startupData.locale); |
| this.textMap = texts.get(this.locale.languageTag); |
| this.textMap.addAll(data.startupData.textMap); |
| |
| // Create the desktop |
| // Extract client session data without creating a model adapter for it. It is (currently) only used to transport the desktop's adapterId. |
| var clientSessionData = this._getAdapterData(data.startupData.clientSession); |
| this.desktop = this.getOrCreateWidget(clientSessionData.desktop, this.rootAdapter.widget); |
| var renderDesktopImpl = function() { |
| this._renderDesktop(); |
| |
| // In case the server sent additional events, process them |
| if (data.events) { |
| this.processingEvents = true; |
| try { |
| this._processEvents(data.events); |
| } finally { |
| this.processingEvents = false; |
| } |
| } |
| |
| // Ensure layout is valid (explicitly layout immediately and don't wait for setTimeout to run to make layouting invisible to the user) |
| this.layoutValidator.validate(); |
| this.focusManager.validateFocus(); |
| |
| // Start poller |
| this._resumeBackgroundJobPolling(); |
| |
| this.ready = true; |
| |
| $.log.isInfoEnabled() && $.log.info('Session initialized. Detected ' + Device.get()); |
| if ($.log.isDebugEnabled()) { |
| $.log.isDebugEnabled() && $.log.debug('size of _adapterDataCache after session has been initialized: ' + objects.countOwnProperties(this._adapterDataCache)); |
| $.log.isDebugEnabled() && $.log.debug('size of modelAdapterRegistry after session has been initialized: ' + objects.countOwnProperties(this.modelAdapterRegistry)); |
| } |
| }.bind(this); |
| |
| this.render(renderDesktopImpl); |
| } |
| |
| _storeClientSessionIdInStorage(clientSessionId) { |
| webstorage.removeItem(sessionStorage, 'scout:clientSessionId'); |
| webstorage.removeItem(localStorage, 'scout:clientSessionId'); |
| var storage = sessionStorage; |
| if (this.persistent) { |
| storage = localStorage; |
| } |
| webstorage.setItem(storage, 'scout:clientSessionId', clientSessionId); |
| } |
| |
| _getClientSessionIdFromStorage() { |
| var id = webstorage.getItem(sessionStorage, 'scout:clientSessionId'); |
| if (!id) { |
| // If the session is persistent it was stored in the local storage (cannot check for this.persistent here because it is not known yet) |
| id = webstorage.getItem(localStorage, 'scout:clientSessionId'); |
| } |
| return id; |
| } |
| |
| render(renderFunc) { |
| // Render desktop after fonts have been preloaded (this fixes initial layouting issues when font icons are not yet ready) |
| if (fonts.loadingComplete) { |
| renderFunc(); |
| } else { |
| fonts.preloader().then(renderFunc); |
| } |
| } |
| |
| _sendUnloadRequest() { |
| var request = this._newRequest({ |
| unload: true, |
| showBusyIndicator: false |
| }); |
| // Send request |
| this._sendRequest(request); |
| } |
| |
| _sendNow() { |
| if (this.asyncEvents.length === 0) { |
| // Nothing to send -> return |
| return; |
| } |
| // If an event requires a new request, only the previous events are sent now. |
| // The next requests are send the next time _sendNow is called (-> when the response to the current request arrives) |
| var events = []; |
| this.asyncEvents.some(function(event, i) { |
| if (event.newRequest && events.length > 0) { |
| return true; |
| } |
| events.push(event); |
| return false; |
| }); |
| var request = this._newRequest({ |
| events: events |
| }); |
| // Busy indicator required when at least one event requests it |
| request.showBusyIndicator = request.events.some(function(event) { |
| return scout.nvl(event.showBusyIndicator, true); |
| }); |
| this.responseQueue.prepareRequest(request); |
| // Send request |
| this._sendRequest(request); |
| // Remove the events which are sent now from the list, keep the ones which are sent later |
| this.asyncEvents = this.asyncEvents.slice(events.length); |
| } |
| |
| _coalesceEvents(previousEvents, event) { |
| if (!event.coalesce) { |
| return previousEvents; |
| } |
| var filter = $.negate(event.coalesce).bind(event); |
| return previousEvents.filter(filter); |
| } |
| |
| _sendRequest(request) { |
| if (!request) { |
| return; // nothing to send |
| } |
| |
| if (this.offline && !request.unload) { // In Firefox, "offline" is already true when page is unloaded |
| this._handleSendWhenOffline(request); |
| return; |
| } |
| |
| if (request.unload && navigator.sendBeacon) { |
| // The unload request must _not_ be sent asynchronously, because the browser would cancel |
| // it when the page unload is completed. Because the support for synchronous AJAX request |
| // will apparently be dropped eventually, we use the "sendBeacon" method to send the unload |
| // request to the server (we don't expect an answer). Not all browsers support this method, |
| // therefore we check for its existence and fall back to (legacy) synchronous AJAX call |
| // when it is missing. More information: |
| // - http://stackoverflow.com/questions/15479103/can-beforeunload-unload-be-used-to-send-xmlhttprequests-reliably |
| // - https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/7nKMdg_ALcc |
| // - https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon |
| navigator.sendBeacon(this.unloadUrl + '/' + this.uiSessionId, ''); |
| return; |
| } |
| |
| var ajaxOptions = this.defaultAjaxOptions(request); |
| |
| var busyHandling = scout.nvl(request.showBusyIndicator, true); |
| if (request.unload) { |
| ajaxOptions.async = false; |
| } |
| this._performUserAjaxRequest(ajaxOptions, busyHandling, request); |
| } |
| |
| _handleSendWhenOffline(request) { |
| // No need to queue the request when request does not contain events (e.g. log request, unload request) |
| if (!request.events) { |
| return; |
| } |
| |
| // Merge request with queued event |
| if (this._queuedRequest) { |
| if (this._queuedRequest.events) { |
| // 1. Remove request events from queued events |
| request.events.forEach(function(event) { |
| this._queuedRequest.events = this._coalesceEvents(this._queuedRequest.events, event); |
| }.bind(this)); |
| // 2. Add request events to end of queued events |
| this._queuedRequest.events = this._queuedRequest.events.concat(request.events); |
| } else { |
| this._queuedRequest.events = request.events; |
| } |
| } else { |
| this._queuedRequest = request; |
| } |
| this.layoutValidator.validate(); |
| } |
| |
| defaultAjaxOptions(request) { |
| request = request || this._newRequest(); |
| var url = this._decorateUrl(this.remoteUrl, request); |
| |
| var ajaxOptions = { |
| type: 'POST', |
| dataType: 'json', |
| contentType: 'application/json; charset=UTF-8', |
| cache: false, |
| url: url, |
| data: this._requestToJson(request) |
| }; |
| |
| // Ensure that certain request don't run forever. When a timeout occurs, the session |
| // is put into offline mode. Note that normal requests should NOT be limited, because |
| // the server processing might take very long (e.g. long running database query). |
| ajaxOptions.timeout = 0; // "infinite" |
| if (request.cancel) { |
| ajaxOptions.timeout = this.requestTimeoutCancel; |
| } |
| if (request.ping) { |
| ajaxOptions.timeout = this.requestTimeoutPing; |
| } |
| if (request.pollForBackgroundJobs) { |
| ajaxOptions.timeout = this.requestTimeoutPoll; |
| } |
| return ajaxOptions; |
| } |
| |
| _decorateUrl(url, request) { |
| var urlHint = null; |
| // Add dummy URL parameter as marker (for debugging purposes) |
| if (request.unload) { |
| urlHint = 'unload'; |
| } else if (request.pollForBackgroundJobs) { |
| urlHint = 'poll'; |
| } else if (request.ping) { |
| urlHint = 'ping'; |
| } else if (request.cancel) { |
| urlHint = 'cancel'; |
| } else if (request.log) { |
| urlHint = 'log'; |
| } else if (request.syncResponseQueue) { |
| urlHint = 'sync'; |
| } |
| if (urlHint) { |
| url = new URL(url).addParameter(urlHint) |
| .toString(); |
| } |
| return url; |
| } |
| |
| _getRequestName(request, defaultName) { |
| if (request) { |
| if (request.unload) { |
| return 'unload'; |
| } else if (request.pollForBackgroundJobs) { |
| return 'pollForBackgroundJobs'; |
| } else if (request.ping) { |
| return 'ping'; |
| } else if (request.cancel) { |
| return 'cancel'; |
| } else if (request.log) { |
| return 'log'; |
| } else if (request.syncResponseQueue) { |
| return 'syncResponseQueue'; |
| } |
| } |
| return defaultName; |
| } |
| |
| _requestToJson(request) { |
| return JSON.stringify(request, function(key, value) { |
| // Replacer function that filter certain properties from the resulting JSON string. |
| // See https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify |
| var ignore = |
| this === request && key === 'showBusyIndicator' || |
| this instanceof RemoteEvent && scout.isOneOf(key, 'showBusyIndicator', 'coalesce', 'newRequest'); |
| return ignore ? undefined : value; |
| }); |
| } |
| |
| _callAjax(callOptions) { |
| var defaultOptions = { |
| retryIntervals: [100, 500, 500, 500] |
| }; |
| var ajaxCall = scout.create('AjaxCall', $.extend(defaultOptions, callOptions, this.ajaxCallOptions), { |
| ensureUniqueId: false |
| }); |
| this.registerAjaxCall(ajaxCall); |
| return ajaxCall.call() |
| .always(this.unregisterAjaxCall.bind(this, ajaxCall)); |
| } |
| |
| _performUserAjaxRequest(ajaxOptions, busyHandling, request) { |
| if (busyHandling) { |
| this.setBusy(true); |
| } |
| this.setRequestPending(true); |
| |
| var jsError = null, |
| success = false; |
| |
| this._callAjax({ |
| ajaxOptions: ajaxOptions, |
| request: request, |
| name: this._getRequestName(request, 'user request') |
| }) |
| .done(onAjaxDone.bind(this)) |
| .fail(onAjaxFail.bind(this)) |
| .always(onAjaxAlways.bind(this)); |
| |
| // ----- Helper methods ----- |
| |
| function onAjaxDone(data) { |
| try { |
| // Busy handling is remove _before_ processing the response, otherwise the focus cannot be set |
| // correctly, because the glasspane of the busy indicator is still visible. |
| // The second check prevents flickering of the busy indicator if there is a scheduled request |
| // that will be sent immediately afterwards (see onAjaxAlways). |
| if (busyHandling && !this.areBusyIndicatedEventsQueued()) { |
| this.setBusy(false); |
| } |
| success = this.responseQueue.process(data); |
| } catch (err) { |
| jsError = jsError || err; |
| } |
| } |
| |
| function onAjaxFail(ajaxError) { |
| try { |
| if (busyHandling) { |
| this.setBusy(false); |
| } |
| this._processErrorResponse(ajaxError.jqXHR, ajaxError.textStatus, ajaxError.errorThrown, request); |
| } catch (err) { |
| jsError = jsError || err; |
| } |
| } |
| |
| // Variable arguments: |
| // "done" --> data, textStatus, jqXHR |
| // "fail" --> jqXHR, textStatus, errorThrown |
| function onAjaxAlways(data, textStatus, errorThrown) { |
| this.setRequestPending(false); |
| |
| // "success" is false when either |
| // a) an HTTP error occurred or |
| // b) a JSON response with the error flag set (UI processing error) was returned |
| if (success) { |
| this._resumeBackgroundJobPolling(); |
| this._fireRequestFinished(data); |
| |
| if (this._retryRequest) { |
| // Send retry request first |
| var retryRequest = this._retryRequest; |
| this._retryRequest = null; |
| this.responseQueue.prepareRequest(retryRequest); |
| this._sendRequest(retryRequest); |
| } else if (this._queuedRequest) { |
| // Send events that happened while being offline |
| var queuedRequest = this._queuedRequest; |
| this._queuedRequest = null; |
| this.responseQueue.prepareRequest(queuedRequest); |
| this._sendRequest(queuedRequest); |
| } |
| |
| // If there already is a another request pending, send it now |
| // But only if it should not be sent delayed |
| if (!this._sendTimeoutId) { |
| this._sendNow(); |
| } |
| } |
| this.layoutValidator.validate(); |
| |
| // Throw previously caught error |
| if (jsError) { |
| throw jsError; |
| } |
| } |
| } |
| |
| registerAjaxCall(ajaxCall) { |
| this.ajaxCalls.push(ajaxCall); |
| } |
| |
| unregisterAjaxCall(ajaxCall) { |
| arrays.remove(this.ajaxCalls, ajaxCall); |
| } |
| |
| interruptAllAjaxCalls() { |
| // Because the error handlers alter the "this.ajaxCalls" array, |
| // the loop must operate on a copy of the original array! |
| this.ajaxCalls.slice().forEach(function(ajaxCall) { |
| ajaxCall.pendingCall && ajaxCall.pendingCall.abort(); |
| }); |
| } |
| |
| abortAllAjaxCalls() { |
| // Because the error handlers alter the "this.ajaxCalls" array, |
| // the loop must operate on a copy of the original array! |
| this.ajaxCalls.slice().forEach(function(ajaxCall) { |
| ajaxCall.abort(); |
| }); |
| } |
| |
| /** |
| * (Re-)starts background job polling when not started yet or when an error occurred while polling. |
| * In the latter case, polling is resumed when a user-initiated request has been successful. |
| */ |
| _resumeBackgroundJobPolling() { |
| if (this.backgroundJobPollingSupport.enabled && this.backgroundJobPollingSupport.status !== BackgroundJobPollingStatus.RUNNING) { |
| $.log.isInfoEnabled() && $.log.info('Resume background jobs polling request, status was=' + this.backgroundJobPollingSupport.status); |
| this._pollForBackgroundJobs(); |
| } |
| } |
| |
| /** |
| * Polls the results of jobs running in the background. Note: we cannot use the _sendRequest method here |
| * since we don't want any busy handling in case of background jobs. The request may take a while, since |
| * the server doesn't return until either a time-out occurs or there's something in the response when |
| * a model job is done and no request initiated by a user is running. |
| */ |
| _pollForBackgroundJobs() { |
| this.backgroundJobPollingSupport.setRunning(); |
| |
| var request = this._newRequest({ |
| pollForBackgroundJobs: true |
| }); |
| this.responseQueue.prepareRequest(request); |
| |
| var ajaxOptions = this.defaultAjaxOptions(request); |
| |
| this._callAjax({ |
| ajaxOptions: ajaxOptions, |
| request: request, |
| name: this._getRequestName(request, 'request') |
| }) |
| .done(onAjaxDone.bind(this)) |
| .fail(onAjaxFail.bind(this)); |
| |
| // --- Helper methods --- |
| |
| function onAjaxDone(data) { |
| if (data.error) { |
| // Don't schedule a new polling request, when an error occurs |
| // when the next user-initiated request succeeds, we re-enable polling |
| // otherwise the polling would ping the server to death in case of an error |
| $.log.warn('Polling request failed. Interrupt polling until the next user-initiated request succeeds'); |
| this.backgroundJobPollingSupport.setFailed(); |
| if (this.areRequestsPending()) { |
| // Add response to queue, handle later by _performUserAjaxRequest() |
| this.responseQueue.add(data); |
| } else { |
| // No user request pending, handle immediately |
| this.responseQueue.process(data); |
| } |
| } else if (data.sessionTerminated) { |
| $.log.info('Session terminated, stopped polling for background jobs'); |
| this.backgroundJobPollingSupport.setStopped(); |
| // If were are not yet logged out, redirect to the logout URL (the session that initiated the |
| // session invalidation will receive a dedicated logout event, redirect is handled there). |
| if (!this.loggedOut && data.redirectUrl) { |
| this.logout(data.redirectUrl); |
| } |
| } else { |
| try { |
| // No need to change backgroundJobPollingSupport state, it should still be RUNNING |
| if (this.areRequestsPending()) { |
| // Add response to queue, handle later by _performUserAjaxRequest() |
| this.responseQueue.add(data); |
| } else { |
| // No user request pending, handle immediately |
| this.responseQueue.process(data); |
| this.layoutValidator.validate(); |
| } |
| setTimeout(this._pollForBackgroundJobs.bind(this)); |
| } catch (error) { |
| this.backgroundJobPollingSupport.setFailed(); |
| throw error; |
| } |
| } |
| } |
| |
| function onAjaxFail(ajaxError) { |
| this.backgroundJobPollingSupport.setFailed(); |
| this._processErrorResponse(ajaxError.jqXHR, ajaxError.textStatus, ajaxError.errorThrown, request); |
| } |
| } |
| |
| /** |
| * Do NOT call this method directly, always use the response queue: |
| * |
| * session.responseQueue.process(data); |
| * |
| * Otherwise, the response queue's expected sequence number will get out of sync. |
| */ |
| processJsonResponseInternal(data) { |
| var success = false; |
| if (data.error) { |
| this._processErrorJsonResponse(data.error); |
| } else { |
| this._processSuccessResponse(data); |
| success = true; |
| } |
| return success; |
| } |
| |
| _processSuccessResponse(message) { |
| if (message.adapterData) { |
| this._copyAdapterData(message.adapterData); |
| } |
| |
| if (message.events) { |
| this.processingEvents = true; |
| try { |
| this._processEvents(message.events); |
| } finally { |
| this.processingEvents = false; |
| } |
| } |
| |
| if ($.log.isDebugEnabled()) { |
| var cacheSize = objects.countOwnProperties(this._adapterDataCache); |
| $.log.trace('size of _adapterDataCache after response has been processed: ' + cacheSize); |
| cacheSize = objects.countOwnProperties(this.modelAdapterRegistry); |
| $.log.trace('size of modelAdapterRegistry after response has been processed: ' + cacheSize); |
| } |
| } |
| |
| _copyAdapterData(adapterData) { |
| var count = 0; |
| var prop; |
| |
| for (prop in adapterData) { |
| this._adapterDataCache[prop] = adapterData[prop]; |
| count++; |
| } |
| if (count > 0) { |
| $.log.isTraceEnabled() && $.log.trace('Stored ' + count + ' properties in adapterDataCache'); |
| } |
| } |
| |
| /** |
| * @param textStatus 'timeout', 'abort', 'error' or 'parseerror' (see http://api.jquery.com/jquery.ajax/) |
| */ |
| _processErrorResponse(jqXHR, textStatus, errorThrown, request) { |
| $.log.error('errorResponse: status=' + jqXHR.status + ', textStatus=' + textStatus + ', errorThrown=' + errorThrown); |
| |
| var offlineError = AjaxCall.isOfflineError(jqXHR, textStatus, errorThrown, request); |
| if (offlineError) { |
| if (this.ready) { |
| this.goOffline(); |
| if (request && !request.pollForBackgroundJobs && !this._retryRequest) { |
| this._retryRequest = request; |
| } |
| return; |
| } |
| // Not ready yet (startup request) |
| errorThrown = errorThrown || this.optText('ui.ConnectionInterrupted', 'Connection interrupted'); |
| } |
| |
| // Show error message |
| var boxOptions = { |
| header: this.optText('ui.NetworkError', 'Network error'), |
| body: strings.join(' ', jqXHR.status || '', errorThrown), |
| yesButtonText: this.optText('ui.Reload', 'Reload'), |
| yesButtonAction: function() { |
| scout.reloadPage(); |
| }, |
| noButtonText: this.ready ? this.optText('ui.Ignore', 'Ignore') : null |
| }; |
| this.showFatalMessage(boxOptions, jqXHR.status + '.net'); |
| } |
| |
| _processErrorJsonResponse(jsonError) { |
| if (jsonError.code === Session.JsonResponseError.VERSION_MISMATCH) { |
| var loopDetection = webstorage.getItem(sessionStorage, 'scout:versionMismatch'); |
| if (!loopDetection) { |
| webstorage.setItem(sessionStorage, 'scout:versionMismatch', 'yes'); |
| // Reload page -> everything should then be up to date |
| scout.reloadPage(); |
| return; |
| } |
| webstorage.removeItem(sessionStorage, 'scout:versionMismatch'); |
| } |
| |
| // Default values for fatal message boxes |
| var boxOptions = { |
| header: this.optText('ui.ServerError', 'Server error') + ' (' + this.optText('ui.ErrorCodeX', 'Code ' + jsonError.code, jsonError.code) + ')', |
| body: jsonError.message, |
| yesButtonText: this.optText('ui.Reload', 'Reload'), |
| yesButtonAction: function() { |
| scout.reloadPage(); |
| } |
| }; |
| |
| // Customize for specific error codes |
| if (jsonError.code === Session.JsonResponseError.STARTUP_FAILED) { |
| // there are no texts yet if session startup failed |
| boxOptions.header = jsonError.message; |
| boxOptions.body = null; |
| boxOptions.yesButtonText = 'Retry'; |
| } else if (jsonError.code === Session.JsonResponseError.SESSION_TIMEOUT) { |
| boxOptions.header = this.optText('ui.SessionTimeout', boxOptions.header); |
| boxOptions.body = this.optText('ui.SessionExpiredMsg', boxOptions.body); |
| } else if (jsonError.code === Session.JsonResponseError.UI_PROCESSING) { |
| boxOptions.header = this.optText('ui.UnexpectedProblem', boxOptions.header); |
| boxOptions.body = strings.join('\n\n', |
| this.optText('ui.InternalProcessingErrorMsg', boxOptions.body, ' (' + this.optText('ui.ErrorCodeX', 'Code 20', '20') + ')'), |
| this.optText('ui.UiInconsistentMsg', '')); |
| boxOptions.noButtonText = this.optText('ui.Ignore', 'Ignore'); |
| } else if (jsonError.code === Session.JsonResponseError.UNSAFE_UPLOAD) { |
| boxOptions.header = this.optText('ui.UnsafeUpload', boxOptions.header); |
| boxOptions.body = this.optText('ui.UnsafeUploadMsg', boxOptions.body); |
| boxOptions.yesButtonText = this.optText('ui.Ok', 'Ok'); |
| boxOptions.yesButtonAction = function() { |
| }; |
| } |
| this.showFatalMessage(boxOptions, jsonError.code); |
| } |
| |
| _fireRequestFinished(message) { |
| if (!this._deferred) { |
| return; |
| } |
| if (message.events) { |
| for (var i = 0; i < message.events.length; i++) { |
| this._deferredEventTypes.push(message.events[i].type); |
| } |
| } |
| if (this.requestsPendingCounter === 0) { |
| this._deferred.resolve(this._deferredEventTypes); |
| this._deferred = null; |
| this._deferredEventTypes = null; |
| } |
| } |
| |
| /** |
| * Shows a UI-only message box. |
| * |
| * @param options |
| * Options for the message box, see MessageBox |
| * @param errorCode |
| * If defined, a second call to this method with the same errorCode will |
| * do nothing. Can be used to prevent double messages for the same error. |
| */ |
| showFatalMessage(options, errorCode) { |
| if (errorCode) { |
| if (this._fatalMessagesOnScreen[errorCode]) { |
| return; |
| } |
| this._fatalMessagesOnScreen[errorCode] = true; |
| } |
| this._setApplicationLoading(false); |
| |
| options = options || {}; |
| var model = { |
| session: this, |
| parent: this.desktop || new NullWidget(), |
| iconId: options.iconId, |
| severity: scout.nvl(options.severity, Status.Severity.ERROR), |
| header: options.header, |
| body: options.body, |
| hiddenText: options.hiddenText, |
| yesButtonText: options.yesButtonText, |
| noButtonText: options.noButtonText, |
| cancelButtonText: options.cancelButtonText |
| }, |
| messageBox = scout.create('MessageBox', model), |
| $entryPoint = options.entryPoint || this.$entryPoint; |
| |
| messageBox.on('action', function(event) { |
| delete this._fatalMessagesOnScreen[errorCode]; |
| messageBox.destroy(); |
| var option = event.option; |
| if (option === 'yes' && options.yesButtonAction) { |
| options.yesButtonAction.apply(this); |
| } else if (option === 'no' && options.noButtonAction) { |
| options.noButtonAction.apply(this); |
| } else if (option === 'cancel' && options.cancelButtonAction) { |
| options.cancelButtonAction.apply(this); |
| } |
| }.bind(this)); |
| messageBox.render($entryPoint); |
| } |
| |
| uploadFiles(target, files, uploadProperties, maxTotalSize, allowedTypes) { |
| var formData = new FormData(), |
| totalSize = 0; |
| |
| if (uploadProperties) { |
| $.each(uploadProperties, function(key, value) { |
| formData.append(key, value); |
| }); |
| } |
| |
| $.each(files, function(index, value) { |
| if (!allowedTypes || allowedTypes.length === 0 || scout.isOneOf(value.type, allowedTypes)) { |
| totalSize += value.size; |
| /* |
| * - see ClipboardField for comments on "scoutName" |
| * - Some Browsers (e.g. Edge) handle an empty string as filename as if the filename is not set and therefore introduce a default filename like 'blob'. |
| * To counter this, we introduce a empty filename string. The string consists of characters that can not occur in regular filenames, to prevent collisions. |
| */ |
| var filename = scout.nvl(value.scoutName, value.name, Session.EMPTY_UPLOAD_FILENAME); |
| formData.append('files', value, filename); |
| } |
| }); |
| |
| // 50 MB as default maximum size |
| maxTotalSize = scout.nvl(maxTotalSize, FileInput.DEFAULT_MAXIMUM_UPLOAD_SIZE); |
| |
| // very large files must not be sent to server otherwise the whole system might crash (for all users). |
| if (totalSize > maxTotalSize) { |
| var boxOptions = { |
| header: this.text('ui.FileSizeLimitTitle'), |
| body: this.text('ui.FileSizeLimit', maxTotalSize / 1024 / 1024), |
| yesButtonText: this.optText('Ok', 'Ok') |
| }; |
| |
| this.showFatalMessage(boxOptions); |
| return false; |
| } |
| |
| var uploadAjaxOptions = { |
| type: 'POST', |
| url: 'upload/' + this.uiSessionId + '/' + target.id, |
| cache: false, |
| // Don't touch the data (do not convert it to string) |
| processData: false, |
| // Do not automatically add content type (otherwise, multipart boundary would be missing) |
| contentType: false, |
| data: formData |
| }; |
| // Special handling for FormData polyfill |
| if (formData.polyfill) { |
| formData.applyToAjaxOptions(uploadAjaxOptions); |
| } |
| this.responseQueue.prepareHttpRequest(uploadAjaxOptions); |
| |
| var busyHandling = !this.areRequestsPending(); |
| this._performUserAjaxRequest(uploadAjaxOptions, busyHandling); |
| return true; |
| } |
| |
| goOffline() { |
| if (this.offline) { |
| return; // already offline |
| } |
| this.offline = true; |
| |
| // Abort pending ajax requests. |
| this.abortAllAjaxCalls(); |
| |
| // In Firefox, the current async polling request is interrupted immediately when the page is unloaded. Therefore, |
| // an offline message would appear at once on the desktop. When reloading the page, all elements are cleared anyway, |
| // thus we wait some short period of time before displaying the message and starting the reconnector. If |
| // we find that goOffline() was called because of request unloading, we skip the unnecessary part. Note that |
| // FF doesn't guarantee that _onWindowUnload() is called before this setTimeout() function is called. Therefore, |
| // we have to look at another property "unloading" that is set earlier in _onWindowBeforeUnload(). |
| setTimeout(function() { |
| if (this.unloading || this.unloaded) { |
| return; |
| } |
| this.rootAdapter.goOffline(); |
| this.reconnector.start(); |
| }.bind(this), 100); |
| } |
| |
| goOnline() { |
| this.offline = false; |
| this.rootAdapter.goOnline(); |
| |
| var request = this._newRequest({ |
| syncResponseQueue: true |
| }); |
| this.responseQueue.prepareRequest(request); |
| this._sendRequest(request); // implies "_resumeBackgroundJobPolling", and also sends queued request |
| } |
| |
| onReconnecting() { |
| if (this.desktop) { |
| this.desktop.onReconnecting(); |
| } |
| } |
| |
| onReconnectingSucceeded() { |
| if (this.desktop) { |
| this.desktop.onReconnectingSucceeded(); |
| } |
| this.goOnline(); |
| } |
| |
| onReconnectingFailed() { |
| if (this.desktop) { |
| this.desktop.onReconnectingFailed(); |
| } |
| } |
| |
| listen() { |
| if (!this._deferred) { |
| this._deferred = $.Deferred(); |
| this._deferredEventTypes = []; |
| } |
| return this._deferred; |
| } |
| |
| /** |
| * Executes the given callback when pending requests are finished, or immediately if there are no requests pending. |
| * @param func callback function |
| * @param vararg arguments to pass to the callback function |
| */ |
| onRequestsDone(func) { |
| var argumentsArray = [...arguments].slice(); |
| argumentsArray.shift(); // remove argument func, remainder: all other arguments |
| |
| if (this.areRequestsPending() || this.areEventsQueued()) { |
| this.listen().done(onEventsProcessed); |
| } else { |
| func.apply(this, argumentsArray); |
| } |
| |
| function onEventsProcessed() { |
| func.apply(this, argumentsArray); |
| } |
| } |
| |
| /** |
| * Executes the given callback when all events of the current response are processed. Executes it immediately if no events are being processed. |
| * @param func callback function |
| * @param vararg arguments to pass to the callback function |
| */ |
| onEventsProcessed(func) { |
| var argumentsArray = [...arguments].slice(); |
| argumentsArray.shift(); // remove argument func, remainder: all other arguments |
| |
| if (this.processingEvents) { |
| this.one('eventsProcessed', execFunc); |
| } else { |
| execFunc(); |
| } |
| |
| function execFunc() { |
| func.apply(this, argumentsArray); |
| } |
| } |
| |
| areEventsQueued() { |
| return this.asyncEvents.length > 0; |
| } |
| |
| areBusyIndicatedEventsQueued() { |
| return this.asyncEvents.some(function(event) { |
| return scout.nvl(event.showBusyIndicator, true); |
| }); |
| } |
| |
| areResponsesQueued() { |
| return this.responseQueue.size() > 0; |
| } |
| |
| areRequestsPending() { |
| return this.requestsPendingCounter > 0; |
| } |
| |
| setRequestPending(pending) { |
| if (pending) { |
| this.requestsPendingCounter++; |
| } else { |
| this.requestsPendingCounter--; |
| } |
| |
| // In "inspector" mode, add/remove a marker attribute to the $entryPoint that |
| // can be used to detect pending server calls by UI testing tools, e.g. Selenium |
| if (this.inspector) { |
| this.$entryPoint.toggleAttr('data-request-pending', pending, 'true'); |
| } |
| } |
| |
| setBusy(busy) { |
| if (busy) { |
| if (!this._busy) { |
| this._renderBusy(); |
| } |
| this._busy = true; |
| } else { |
| if (this._busy) { |
| this._removeBusy(); |
| } |
| this._busy = false; |
| } |
| } |
| |
| _renderBusy() { |
| if (this._busyIndicatorTimeoutId !== null && this._busyIndicatorTimeoutId !== undefined) { |
| // Do not schedule it twice |
| return; |
| } |
| // Don't show the busy indicator immediately. Set a short timer instead (which may be |
| // cancelled again if the busy state returns to false in the meantime). |
| this._busyIndicatorTimeoutId = setTimeout(function() { |
| if (this._busyIndicator) { |
| // busy indicator is already showing |
| return; |
| } |
| if (!this.desktop || !this.desktop.rendered) { |
| return; // No busy indicator without desktop (e.g. during shutdown) |
| } |
| this._busyIndicator = scout.create('BusyIndicator', { |
| parent: this.desktop |
| }); |
| this._busyIndicator.on('cancel', this._onCancelProcessing.bind(this)); |
| this._busyIndicator.render(this.$entryPoint); |
| }.bind(this), 500); |
| } |
| |
| _removeBusy() { |
| // Clear pending timer |
| clearTimeout(this._busyIndicatorTimeoutId); |
| this._busyIndicatorTimeoutId = null; |
| |
| // Remove busy indicator (if it was already created) |
| if (this._busyIndicator) { |
| this._busyIndicator.destroy(); |
| this._busyIndicator = null; |
| } |
| } |
| |
| _onCancelProcessing(event) { |
| var busyIndicator = this._busyIndicator; |
| if (!busyIndicator) { |
| return; // removed in the mean time |
| } |
| busyIndicator.off('cancel'); |
| |
| // Set "canceling" state in busy indicator (after 100ms, would not look good otherwise) |
| setTimeout(function() { |
| busyIndicator.cancelled(); |
| }, 100); |
| |
| this._sendCancelRequest(); |
| } |
| |
| _sendCancelRequest() { |
| var request = this._newRequest({ |
| cancel: true, |
| showBusyIndicator: false |
| }); |
| this._sendRequest(request); |
| } |
| |
| /** |
| * Sends a request containing the error message for logging purpose. |
| * The request is sent immediately (does not await pending requests) |
| */ |
| sendLogRequest(message) { |
| var request = this._newRequest({ |
| log: true, |
| message: message |
| }); |
| if (this.currentEvent) { |
| request.event = { |
| target: this.currentEvent.target, |
| type: this.currentEvent.type |
| }; |
| } |
| |
| // Do not use _sendRequest to make sure a log request has no side effects and will be sent only once |
| $.ajax(this.defaultAjaxOptions(request)); |
| } |
| |
| _newRequest(requestData) { |
| var request = $.extend({ |
| uiSessionId: this.uiSessionId |
| }, requestData); |
| |
| // Certain requests do not require a sequence number |
| if (!request.log && !request.syncResponseQueue) { |
| request['#'] = this.requestSequenceNo++; |
| } |
| return request; |
| } |
| |
| _setApplicationLoading(applicationLoading) { |
| if (applicationLoading) { |
| this._applicationLoadingTimeoutId = setTimeout(function() { |
| if (!this.desktop || !this.desktop.rendered) { |
| this._renderApplicationLoading(); |
| } |
| }.bind(this), 200); |
| } else { |
| clearTimeout(this._applicationLoadingTimeoutId); |
| this._applicationLoadingTimeoutId = null; |
| this._removeApplicationLoading(); |
| } |
| } |
| |
| _renderApplicationLoading() { |
| var $loadingRoot = $('body').appendDiv('application-loading-root') |
| .addClass('application-loading-root') |
| .fadeIn(); |
| $loadingRoot.appendDiv('application-loading01').hide() |
| .fadeIn(); |
| $loadingRoot.appendDiv('application-loading02').hide() |
| .fadeIn(); |
| } |
| |
| _removeApplicationLoading() { |
| var $loadingRoot = $('body').children('.application-loading-root'); |
| $loadingRoot.addClass('application-loading-root-fadeout'); |
| if (Device.get().supportsCssAnimation()) { |
| $loadingRoot.oneAnimationEnd(function() { |
| $loadingRoot.remove(); |
| }); |
| } else { |
| // fallback for old browsers that do not support the animation-end event |
| $loadingRoot.remove(); |
| } |
| } |
| |
| _processEvents(events) { |
| var i = 0; |
| while (i < events.length) { |
| var event = events[i]; |
| this.currentEvent = event; |
| |
| var adapter = this.getModelAdapter(event.target); |
| if (!adapter) { |
| // Sometimes events seem to happen "too early", e.g. when a "requestFocus" event for a field is |
| // encountered before the "showForm" event has been processed. If the target adapter cannot be |
| // resolved, we try the other events first, expecting them to trigger the creation of the event |
| // adapter. As soon as a event could be processed successfully, we try our postponed event again. |
| $.log.isDebugEnabled() && $.log.debug('Postponing \'' + event.type + '\' for adapter with ID ' + event.target); |
| i++; |
| continue; |
| } |
| // Remove the successful event and reset the pointer to the start of the remaining events (to |
| // retry previously postponed events). |
| events.splice(i, 1); |
| i = 0; |
| |
| $.log.isDebugEnabled() && $.log.debug('Processing event \'' + event.type + '\' for adapter with ID ' + event.target); |
| adapter.onModelEvent(event); |
| adapter.resetEventFilters(); |
| } |
| this.currentEvent = null; |
| |
| // If there are still events whose target could not be resolved, throw an error |
| if (events.length) { |
| throw new Error('Could not resolve event targets: [' + events.map(function(event) { |
| var msg = 'target: ' + event.target + ', type: ' + event.type; |
| if (event.properties) { |
| msg += ', properties: ' + Object.keys(event.properties); |
| } |
| return '"' + msg + '"'; |
| }, this).join(', ') + ']'); |
| } |
| this.trigger('eventsProcessed'); |
| } |
| |
| start() { |
| $.log.isInfoEnabled() && $.log.info('Session starting...'); |
| |
| // After a short time, display a loading animation (will be removed again in _renderDesktop) |
| this._setApplicationLoading(true); |
| |
| // Send startup request |
| return this._sendStartupRequest(); |
| } |
| |
| onModelEvent(event) { |
| if (event.type === 'localeChanged') { |
| this._onLocaleChanged(event); |
| } else if (event.type === 'logout') { |
| this._onLogout(event); |
| } else if (event.type === 'disposeAdapter') { |
| this._onDisposeAdapter(event); |
| } else if (event.type === 'reloadPage') { |
| this._onReloadPage(event); |
| } else { |
| $.log.warn('Model action "' + event.type + '" is not supported by UI session'); |
| } |
| } |
| |
| resetEventFilters() { |
| // NOP |
| } |
| |
| _onLocaleChanged(event) { |
| var locale = new Locale(event.locale); |
| var textMap = new TextMap(event.textMap); |
| this.switchLocale(locale, textMap); |
| } |
| |
| /** |
| * @param {Locale} the new locale |
| * @param {TextMap} [textMap] the new textMap. If not defined, the corresponding textMap for the new locale is used. |
| */ |
| switchLocale(locale, textMap) { |
| scout.assertParameter('locale', locale, Locale); |
| this.locale = locale; |
| this.textMap = texts.get(locale.languageTag); |
| if (textMap) { |
| objects.copyOwnProperties(textMap, this.textMap); |
| } |
| // TODO [7.0] bsh: inform components to reformat display text? also check Collator in comparators.TEXT |
| |
| this.trigger('localeSwitch', { |
| locale: this.locale |
| }); |
| } |
| |
| _renderDesktop() { |
| this.desktop.render(this.$entryPoint); |
| this.desktop.invalidateLayoutTree(false); |
| this._setApplicationLoading(false); |
| } |
| |
| _onLogout(event) { |
| this.logout(event.redirectUrl); |
| } |
| |
| logout(logoutUrl) { |
| this.loggedOut = true; |
| // TODO [7.0] bsh: Check if there is a better solution (e.g. send a flag from server "action" = [ "redirect" | "closeWindow" ]) |
| if (this.forceNewClientSession) { |
| this.desktop.$container.window(true).close(); |
| } else { |
| // remember current url to not lose query parameters (such as debug; however, ignore deeplinks) |
| var url = new URL(); |
| url.removeParameter('dl'); // deeplink |
| url.removeParameter('i'); // deeplink info |
| webstorage.setItem(sessionStorage, 'scout:loginUrl', url.toString()); |
| // Clear everything and reload the page. We wrap that in setTimeout() to allow other events to be executed normally before. |
| setTimeout(function() { |
| scout.reloadPage({ |
| redirectUrl: logoutUrl |
| }); |
| }); |
| } |
| } |
| |
| _onDisposeAdapter(event) { |
| // Model adapter was disposed on server -> dispose it on the UI, too |
| var adapter = this.getModelAdapter(event.adapter); |
| if (adapter) { // adapter may be null if it was never sent to the UI, e.g. a form that was opened and closed in the same request |
| adapter.destroy(); |
| } |
| } |
| |
| _onReloadPage(event) { |
| // Don't clear the body, because other events might be processed before the reload and |
| // it could cause errors when all DOM elements are already removed. |
| scout.reloadPage({ |
| clearBody: false |
| }); |
| } |
| |
| _onWindowBeforeUnload() { |
| $.log.isInfoEnabled() && $.log.info('Session before unloading...'); |
| // TODO [7.0] bsh: Cancel pending requests |
| |
| // Set a flag that indicates unloading before _onWindowUnload() is called. |
| // See goOffline() why this is necessary. |
| this.unloading = true; |
| setTimeout(function() { |
| // Because there is no callback when the unloading was cancelled, we always |
| // reset the flag after a short period of time. |
| this.unloading = false; |
| }.bind(this), 200); |
| } |
| |
| _onWindowUnload() { |
| $.log.isInfoEnabled() && $.log.info('Session unloading...'); |
| this.unloaded = true; |
| |
| // Close popup windows |
| if (this.desktop && this.desktop.formController) { |
| this.desktop.formController.closePopupWindows(); |
| } |
| |
| // Destroy UI session on server (only when the server did not not initiate the logout, |
| // otherwise the UI session would already be disposed) |
| if (!this.loggedOut) { |
| this._sendUnloadRequest(); |
| } |
| if (this.loggedOut && this.persistent) { |
| webstorage.removeItem(localStorage, 'scout:clientSessionId'); |
| } |
| } |
| |
| /** |
| * Returns the adapter-data sent with the JSON response from the adapter-data cache. Note that this operation |
| * removes the requested element from the cache, thus you cannot request the same ID twice. Typically once |
| * you've requested an element from this cache an adapter for that ID is created and stored in the adapter |
| * registry which too exists on this session object. |
| */ |
| _getAdapterData(id) { |
| var adapterData = this._adapterDataCache[id]; |
| var deleteAdapterData = !this.adapterExportEnabled; |
| if (deleteAdapterData) { |
| delete this._adapterDataCache[id]; |
| } |
| return adapterData; |
| } |
| |
| getAdapterData(id) { |
| return this._adapterDataCache[id]; |
| } |
| |
| text(textKey) { |
| return this.textMap.get(...arguments); |
| } |
| |
| optText(textKey, defaultValue) { |
| return this.textMap.optGet(...arguments); |
| } |
| |
| textExists(textKey) { |
| return this.textMap.exists(textKey); |
| } |
| |
| // --- 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); |
| } |
| } |