UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

581 lines (509 loc) 18.6 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*eslint-disable max-len */ // Provides class sap.ui.model.odata.ODataAnnotations sap.ui.define([ "./AnnotationParser", "sap/base/assert", "sap/base/Log", "sap/base/util/extend", "sap/base/util/isEmptyObject", "sap/ui/base/EventProvider", "sap/ui/core/Configuration", "sap/ui/thirdparty/jquery" ], function (AnnotationParser, assert, Log, extend, isEmptyObject, EventProvider, Configuration, jQuery) { "use strict"; /** * @param {string|string[]} aAnnotationURI The annotation-URL or an array of URLs that should be parsed and merged * @param {sap.ui.model.odata.ODataMetadata} oMetadata * @param {object} mParams * * @class Implementation to access OData Annotations * * @author SAP SE * @version * 1.111.5 * * @public * @deprecated As of version 1.66, please use {@link sap.ui.model.odata.v2.ODataAnnotations} instead. * @alias sap.ui.model.odata.ODataAnnotations * @extends sap.ui.base.EventProvider */ var ODataAnnotations = EventProvider.extend("sap.ui.model.odata.ODataAnnotations", /** @lends sap.ui.model.odata.ODataAnnotations.prototype */ { // constructor : function(aAnnotationURI, oMetadata, mParams) { constructor : function(mOptions) { EventProvider.apply(this, arguments); if (arguments.length !== 1) { // Old constructor argument syntax if (typeof arguments[2] === "object") { mOptions = arguments[2]; } mOptions.urls = arguments[0]; mOptions.metadata = arguments[1]; } this.oMetadata = mOptions.metadata; this.oAnnotations = mOptions.annotationData ? mOptions.annotationData : {}; this.bLoaded = false; this.bAsync = mOptions && mOptions.async; this.xPath = null; this.oError = null; this.bValidXML = true; this.oRequestHandles = []; this.oLoadEvent = null; this.oFailedEvent = null; this.mCustomHeaders = mOptions.headers ? extend({}, mOptions.headers) : {}; if (mOptions.urls) { this.addUrl(mOptions.urls); if (!this.bAsync) { // Synchronous loading, we can directly check for errors assert( !isEmptyObject(this.oMetadata), "Metadata must be available for synchronous annotation loading" ); if (this.oError) { Log.error( "OData annotations could not be loaded: " + this.oError.message ); } } } }, metadata : { publicMethods : ["parse", "getAnnotationsData", "attachFailed", "detachFailed", "attachLoaded", "detachLoaded"] } }); ///////////////////////////////////////////////// Prototype Members //////////////////////////////////////////////// /** * Returns the raw annotation data * * @private * @returns {object} the annotation data */ ODataAnnotations.prototype.getData = function() { return this.oAnnotations; }; /** * returns the raw annotation data * * @public * @returns {object} returns annotations data */ ODataAnnotations.prototype.getAnnotationsData = ODataAnnotations.prototype.getData; /** * Checks whether annotations from at least one source are available * * @public * @returns {boolean} returns whether annotations is already loaded */ ODataAnnotations.prototype.isLoaded = function() { return this.bLoaded; }; /** * Checks whether annotations loading of at least one of the given URLs has already failed. * Note: For asynchronous annotations {@link #attachFailed} has to be used. * * @public * @returns {boolean} whether annotations request has failed */ ODataAnnotations.prototype.isFailed = function() { return this.oError !== null; }; /** * The <code>loaded</code> event is fired after new annotations have been added to this object. * * @name sap.ui.model.odata.ODataAnnotations#loaded * @event * @param {sap.ui.base.Event} oEvent * @public */ /** * Fires event {@link #event:loaded loaded} to attached listeners. * * @param {object} [oParameters] Parameters that will be given as parameters to the event handler * @return {this} <code>this</code> to allow method chaining * @protected */ ODataAnnotations.prototype.fireLoaded = function(oParameters) { this.fireEvent("loaded", oParameters); return this; }; /** * Attaches event handler <code>fnFunction</code> to the {@link #event:loaded loaded} event of this * <code>sap.ui.model.odata.ODataAnnotations</code>. * * @param {object} * [oData] An application-specific payload object that will be passed to the event handler * along with the event object when firing the event * @param {function} * fnFunction The function to be called, when the event occurs * @param {object} * [oListener] Context object to call the event handler with. Defaults to this * <code>sap.ui.model.odata.ODataAnnotations</code> itself * * @returns {this} Reference to <code>this</code> in order to allow method chaining * @public */ ODataAnnotations.prototype.attachLoaded = function(oData, fnFunction, oListener) { this.attachEvent("loaded", oData, fnFunction, oListener); return this; }; /** * Detaches event handler <code>fnFunction</code> from the {@link #event:loaded loaded} event of this * <code>sap.ui.model.odata.ODataAnnotations</code>. * * The passed function and listener object must match the ones used for event registration. * * @param {function} * fnFunction The function to be called, when the event occurs * @param {object} * [oListener] Context object on which the given function had to be called * @returns {this} Reference to <code>this</code> in order to allow method chaining * @public */ ODataAnnotations.prototype.detachLoaded = function(fnFunction, oListener) { this.detachEvent("loaded", fnFunction, oListener); return this; }; /** * The <code>failed</code> event is fired when loading, parsing or merging new annotations failed. * * @name sap.ui.model.odata.ODataAnnotations#failed * @event * @param {sap.ui.base.Event} oEvent * @public */ /** * Fires event {@link #event:failed failed} to attached listeners. * * @param {object} [oParameters] Parameters to pass along with the event * @param {string} [oParameters.message] A text that describes the failure. * @param {string} [oParameters.statusCode] HTTP status code returned by the request (if available) * @param {string} [oParameters.statusText] The status as a text, details not specified, intended only for diagnosis output * @param {string} [oParameters.responseText] Response that has been received for the request ,as a text string * * @returns {this} Reference to <code>this</code> in order to allow method chaining * @protected */ ODataAnnotations.prototype.fireFailed = function(oParameters) { this.fireEvent("failed", oParameters); return this; }; /** * Attaches event handler <code>fnFunction</code> to the {@link #event:failed failed} event of this * <code>sap.ui.model.odata.ODataAnnotations</code>. * * * @param {object} * [oData] An application-specific payload object that will be passed to the event handler * along with the event object when firing the event * @param {function} * fnFunction The function to be called, when the event occurs * @param {object} * [oListener] Context object to call the event handler with. Defaults to this * <code>sap.ui.model.odata.ODataAnnotations</code> itself * * @returns {this} Reference to <code>this</code> in order to allow method chaining * @public */ ODataAnnotations.prototype.attachFailed = function(oData, fnFunction, oListener) { this.attachEvent("failed", oData, fnFunction, oListener); return this; }; /** * Detaches event handler <code>fnFunction</code> from the {@link #event:failed failed} event of this * <code>sap.ui.model.odata.ODataAnnotations</code>. * * The passed function and listener object must match the ones used for event registration. * * @param {function} * fnFunction The function to be called, when the event occurs * @param {object} * [oListener] Context object on which the given function had to be called * @returns {this} Reference to <code>this</code> in order to allow method chaining * @public */ ODataAnnotations.prototype.detachFailed = function(fnFunction, oListener) { this.detachEvent("failed", fnFunction, oListener); return this; }; /** * Set custom headers which are provided in a key/value map. These headers are used for all requests. * The Accept-Language header cannot be modified and is set using the Core's language setting. * * To remove these headers simply set the mHeaders parameter to {}. Please also note that when calling this method * again all previous custom headers are removed unless they are specified again in the mCustomHeaders parameter. * * @param {Object<string,string>} mHeaders the header name/value map. * @public */ ODataAnnotations.prototype.setHeaders = function(mHeaders) { // Copy headers (dont use reference to mHeaders map) this.mCustomHeaders = extend({}, mHeaders); }; /** * Creates an XML document that can be used by this parser from the given XML content. * * @param {object|string} vXML Either an XML Document to be used for parsing or a string that should be parsed as an XML document. In case the first parameter is an object, the second parameter must be set to ensure browser compatibility * @param {string} [sXMLContent] Fallback XML content as string in case the first parameter was an object and could not be used * @returns {object} The compatible XML document object * @private */ ODataAnnotations.prototype._createXMLDocument = function(vXML, sXMLContent) { var oXMLDoc = null; if (typeof vXML === "string") { sXMLContent = vXML; vXML = null; } if (vXML) { oXMLDoc = vXML; } else { oXMLDoc = new DOMParser().parseFromString(sXMLContent, 'application/xml'); } return oXMLDoc; }; /** * Checks the given XML document for parse errors * * @param {object} oXMLDoc The XML document object * @return {boolean} true if errors exist false otherwise */ ODataAnnotations.prototype._documentHasErrors = function(oXMLDoc) { return oXMLDoc.getElementsByTagName("parsererror").length > 0; }; /** * Merges the newly parsed annotation data into the already existing one. * The merge operates on Terms and overwrites existing annotations on that level. * * @param {map} mAnnotations The new annotations that should be merged into the ones in this instance * @param {boolean} [bSuppressEvents] if set to true, the "loaded"-event is not fired */ ODataAnnotations.prototype._mergeAnnotationData = function(mAnnotations, bSuppressEvents) { if (!this.oAnnotations) { this.oAnnotations = {}; } AnnotationParser.merge(this.oAnnotations, mAnnotations); this.bLoaded = true; if (!bSuppressEvents) { this.fireLoaded({ annotations: mAnnotations }); } }; /** * Sets an XML document. * * @param {object} oXMLDocument The XML document to parse for annotations * @param {string} sXMLContent The XML content as string to parse for annotations * @param {object} [mOptions] Additional options * @param {function} [mOptions.success] Success callback gets an objec as argument with the * properties "annotations" containing the parsed annotations and "xmlDoc" * containing the XML-Document that was returned by the request. * @param {function} [mOptions.error] Error callback gets an objec as argument with the * property "xmlDoc" containing the XML-Document that was returned by the * request and could not be correctly parsed. * @param {boolean} [mOptions.fireEvents] If this option is set to true, events are fired as if the annotations * were loaded from a URL * @return {boolean} Whether or not parsing was successful * @public */ ODataAnnotations.prototype.setXML = function(oXMLDocument, sXMLContent, mOptions) { // Make sure there are always callable handlers var mDefaultOptions = { success: function() {}, error: function() {}, fireEvents: false }; mOptions = extend({}, mDefaultOptions, mOptions); var oXMLDoc = this._createXMLDocument(oXMLDocument, sXMLContent); var fnParseDocument = function(oXMLDoc) { var mResult = { xmlDoc : oXMLDoc }; var oAnnotations = AnnotationParser.parse(this.oMetadata, oXMLDoc); if (oAnnotations) { mResult.annotations = oAnnotations; mOptions.success(mResult); this._mergeAnnotationData(oAnnotations, !mOptions.fireEvents); } else { mOptions.error(mResult); if (mOptions.fireEvents) { this.fireFailed(mResult); } } }.bind(this, oXMLDoc); if (this._documentHasErrors(oXMLDoc)) { // Malformed XML, notify application of the problem // This seems to be needed since with some jQuery versions the XML document // is partly parsed and with some it is not parsed at all. We now choose the // "safe" approach and only accept completely valid documents. mOptions.error({ xmlDoc : oXMLDoc }); return false; } else { // Check if Metadata is loaded on the model. We need the Metadata to parse the annotations var oMetadata = this.oMetadata.getServiceMetadata(); if (!oMetadata || isEmptyObject(oMetadata)) { // Metadata is not loaded, wait for it before trying to parse this.oMetadata.attachLoaded(fnParseDocument); } else { fnParseDocument(); } return true; } }; /** * Adds either one URL or an array of URLs to be loaded and parsed. The result will be merged into the annotations * data which can be retrieved using the getAnnotations-method. * * @param {string|string[]} vUrl Either one URL as string or an array of URL strings * @return {Promise} The Promise to load the given URL(s), resolved if all URLs have been loaded, rejected if at * least one failed to load. The argument is an object containing the annotations object, success (an array * of sucessfully loaded URLs), fail (an array ob of failed URLs). * @public */ ODataAnnotations.prototype.addUrl = function(vUrl) { var that = this; var aUris = vUrl; if (Array.isArray(vUrl) && vUrl.length == 0) { return Promise.resolve({annotations: this.oAnnotations}); } if (!Array.isArray(vUrl)) { aUris = [ vUrl ]; } return new Promise(function(fnResolve, fnReject) { var iLoadCount = 0; var mResults = { annotations: null, success: [], fail: [] }; var fnRequestCompleted = function(mResult) { iLoadCount++; if (mResult.type === "success") { mResults.success.push(mResult); } else { mResults.fail.push(mResult); } if (iLoadCount === aUris.length) { // Finished loading all URIs mResults.annotations = that.oAnnotations; if (mResults.success.length > 0) { // For compatibility reasons, we fire the loaded event if at least one has been loaded... var mSuccess = { annotations: that.oAnnotations, results: mResults }; that.fireLoaded(mSuccess); } if (mResults.success.length < aUris.length) { // firefailed is called for every failed URL in _loadFromUrl var oError = new Error("At least one annotation failed to load/parse/merge"); oError.annotations = mResults.annotations; oError.success = mResults.success; oError.fail = mResults.fail; fnReject(oError); } else { // All URLs could be loaded and parsed fnResolve(mResults); } } }; var i = 0; if (that.bAsync) { var promiseChain = Promise.resolve(); for (i = 0; i < aUris.length; ++i) { var fnLoadNext = that._loadFromUrl.bind(that, aUris[i]); promiseChain = promiseChain .then(fnLoadNext, fnLoadNext) .then(fnRequestCompleted, fnRequestCompleted); } } else { for (i = 0; i < aUris.length; ++i) { that._loadFromUrl(aUris[i]).then(fnRequestCompleted, fnRequestCompleted); } } }); }; /** * Returns a promise to load and parse annotations from a single URL, resolves if the URL could be loaded and parsed, rejects * otherwise * * @param {string} sUrl The URL to load * @return {Promise} The promise to load the URL. Argument contains information about the failed or succeeded request */ ODataAnnotations.prototype._loadFromUrl = function(sUrl) { var that = this; return new Promise(function(fnResolve, fnReject) { var mAjaxOptions = { url: sUrl, async: that.bAsync, headers: extend({}, that.mCustomHeaders, { "Accept-Language": Configuration.getLanguageTag() // Always overwrite }) }; var oRequestHandle; var fnFail = function(oJQXHR, sStatusText) { if (oRequestHandle && oRequestHandle.bSuppressErrorHandlerCall) { return; } that.oError = { type: "fail", url: sUrl, message: sStatusText, statusCode: oJQXHR.status, statusText: oJQXHR.statusText, responseText: oJQXHR.responseText }; if (that.bAsync) { that.oFailedEvent = setTimeout(that.fireFailed.bind(that, that.oError), 0); } else { that.fireFailed(that.oError); } fnReject(that.oError); }; var fnSuccess = function(sData, sStatusText, oJQXHR) { that.setXML(oJQXHR.responseXML, oJQXHR.responseText, { success: function(mData) { fnResolve({ type: "success", url: sUrl, message: sStatusText, statusCode: oJQXHR.status, statusText: oJQXHR.statusText, responseText: oJQXHR.responseText }); }, error : function(mData) { fnFail(oJQXHR, "Malformed XML document"); }, url: sUrl }); }; jQuery.ajax(mAjaxOptions).done(fnSuccess).fail(fnFail); }); }; ODataAnnotations.prototype.destroy = function() { // Abort pending xml request for (var i = 0; i < this.oRequestHandles.length; ++i) { if (this.oRequestHandles[i]) { this.oRequestHandles[i].bSuppressErrorHandlerCall = true; this.oRequestHandles[i].abort(); this.oRequestHandles[i] = null; } } EventProvider.prototype.destroy.apply(this, arguments); if (this.oLoadEvent) { clearTimeout(this.oLoadEvent); } if (this.oFailedEvent) { clearTimeout(this.oFailedEvent); } }; return ODataAnnotations; });