@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
966 lines (858 loc) • 35.4 kB
JavaScript
/*!
* 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 */
sap.ui.define([
"sap/ui/model/odata/ODataMetadata",
"sap/ui/model/odata/ODataUtils",
"sap/ui/core/library",
"sap/ui/thirdparty/URI",
"sap/ui/core/message/MessageParser",
"sap/ui/core/message/Message",
"sap/base/Log"
],
function(ODataMetadata, ODataUtils, coreLibrary, URI, MessageParser, Message, Log) {
"use strict";
var sClassName = "sap.ui.model.odata.ODataMessageParser",
rEnclosingSlashes = /^\/+|\/$/g,
// shortcuts for enums
MessageType = coreLibrary.MessageType,
// This map is used to translate back-end response severity values to the values defined in the
// enumeration sap.ui.core.MessageType
mSeverity2MessageType = {
"error" : MessageType.Error,
"info" : MessageType.Information,
"success" : MessageType.Success,
"warning" : MessageType.Warning
};
/**
* A plain error object as returned by the server. Either "@sap-severity"- or "severity"-property
* must be set.
*
* @typedef {object} ODataMessageParser~ServerError
* @property {string} target - The target entity path for which the message is meant
* @property {string} message - The error message description
* @property {string} code - The error code (message)
* @property {string} [@sap-severity] - The level of the error (alternatively in V2: oMessageObject.severity) can be one of "success", "info", "warning", "error"
* @property {string} [severity] - The level of the error (alternatively in V4: oMessageObject.@sap-severity) can be one of "success", "info", "warning", "error"
*/
/**
* A map containing the relevant request-URL and (if available) the request and response objects
*
* @typedef {object} ODataMessageParser~RequestInfo
* @property {string} url - The URL of the request
* @property {object} request - The request object
* @property {object} response - The response object
*/
/**
* A map containing a parsed URL
*
* @typedef {object} ODataMessageParser~UrlInfo
* @property {string} url - The URL, stripped of query and hash
* @property {Object<string,string>} parameters - A map of the query parameters
* @property {string} hash - The hash value of the URL
*/
/**
*
* @namespace
* @name sap.ui.model.odata
* @public
*/
/**
* OData implementation of the sap.ui.core.message.MessageParser class. Parses message responses
* from the back end.
*
* @param {string} sServiceUrl
* Base URI of the service used for the calculation of message targets
* @param {sap.ui.model.odata.ODataMetadata} oMetadata
* The ODataMetadata object
* @param {boolean} bPersistTechnicalMessages
* Whether technical messages should always be treated as persistent, since 1.83.0
*
* @class
* OData implementation of the sap.ui.core.message.MessageParser class. Parses message responses
* from the back end.
* @extends sap.ui.core.message.MessageParser
*
* @author SAP SE
* @version 1.111.5
* @public
* @alias sap.ui.model.odata.ODataMessageParser
*/
var ODataMessageParser = MessageParser.extend("sap.ui.model.odata.ODataMessageParser", {
metadata: {
publicMethods: [ "parse", "setProcessor", "getHeaderField", "setHeaderField" ]
},
constructor: function(sServiceUrl, oMetadata, bPersistTechnicalMessages) {
MessageParser.apply(this);
this._serviceUrl = getRelativeServerUrl(this._parseUrl(sServiceUrl).url);
this._metadata = oMetadata;
this._headerField = "sap-message"; // Default header field
this._lastMessages = [];
this._bPersistTechnicalMessages = bPersistTechnicalMessages;
}
});
////////////////////////////////////////// Public Methods //////////////////////////////////////////
/**
* Returns the name of the header field that is used to parse the server messages
*
* @return {string} The name of the header field
* @public
*/
ODataMessageParser.prototype.getHeaderField = function() {
return this._headerField;
};
/**
* Sets the header field name that should be used for parsing the JSON messages
*
* @param {string} sFieldName - The name of the header field that should be used as source of the message object
* @return {this} Instance reference for method chaining
* @public
*/
ODataMessageParser.prototype.setHeaderField = function(sFieldName) {
this._headerField = sFieldName;
return this;
};
/**
* Parses the given response for messages, calculates the delta and fires the messageChange-event
* on the MessageProcessor if messages are found. Messages of responses to GET requests with status
* codes 204 or 424 are ignored.
*
* @param {object} oResponse
* The response from the server containing body and headers
* @param {object} oRequest
* The original request that lead to this response
* @param {object} [mGetEntities]
* A map with the keys of the entities requested from the back-end mapped to true
* @param {object} [mChangeEntities]
* A map with the keys of the entities changed in the back-end mapped to true
* @param {boolean} [bMessageScopeSupported]
* Whether the used OData service supports the message scope
* {@link sap.ui.model.odata.MessageScope.BusinessObject}
* @public
*/
ODataMessageParser.prototype.parse = function(oResponse, oRequest, mGetEntities, mChangeEntities,
bMessageScopeSupported) {
var aMessages,
mRequestInfo,
sStatusCode = String(oResponse.statusCode);
if (oRequest.method === "GET" && sStatusCode === "204") {
return;
}
mRequestInfo = {
request: oRequest,
response: oResponse,
url: oRequest.requestUri
};
if (oResponse.statusCode >= 200 && oResponse.statusCode < 300) {
// Status is 2XX - parse headers
aMessages = this._parseHeader(oResponse, mRequestInfo);
} else if (oResponse.statusCode >= 400 && oResponse.statusCode < 600) {
// Status us 4XX or 5XX - parse body
try {
aMessages = this._parseBody(oResponse, mRequestInfo);
} catch (ex) {
aMessages = this._createGenericError(mRequestInfo);
Log.debug("Failed to parse error messages from the response body", ex, sClassName);
} finally {
this._logErrorMessages(aMessages, oRequest, sStatusCode);
}
} else {
// Status neither ok nor error, may happen if no network connection is available (some
// browsers use status code 0 in that case)
aMessages = this._createGenericError(mRequestInfo);
Log.error("Request failed with unsupported status code " + sStatusCode + ": "
+ oRequest.method + " " + oRequest.requestUri, undefined, sClassName);
}
if (oRequest.method === "GET" && sStatusCode === "424") {
// Failed dependency: End user message already created for superordinate request
return;
}
this._propagateMessages(aMessages, mRequestInfo, mGetEntities, mChangeEntities,
!bMessageScopeSupported);
};
////////////////////////////////////////// onEvent Methods /////////////////////////////////////////
////////////////////////////////////////// Private Methods /////////////////////////////////////////
/**
* Computes the affected targets from the given messages contained in the response for the given
* request, the request and entities read from or changed in the back-end.
* These "affected targets" are used to check which currently available messages should be replaced
* with the new ones.
*
* @param {sap.ui.core.message.Message[]} aMessages
* All messages returned from the back-end in this request
* @param {object} mRequestInfo
* The request info
* @param {object} mGetEntities
* A map with the keys of the entities requested from the back-end mapped to true
* @param {object} mChangeEntities
* A map with the keys of the entities changed in the back-end mapped to true
* @returns {object}
* A map of affected targets as keys mapped to true
*/
ODataMessageParser.prototype._getAffectedTargets = function (aMessages, mRequestInfo, mGetEntities,
mChangeEntities) {
// unbound messages are always affected => add target ""
var mAffectedTargets = Object.assign({"" : true}, mGetEntities, mChangeEntities),
oEntitySet,
sRequestTarget = this._parseUrl(mRequestInfo.url).url;
if (mRequestInfo.request.key && mRequestInfo.request.created){
mAffectedTargets[mRequestInfo.request.key] = true;
}
if (sRequestTarget.startsWith(this._serviceUrl)) {
sRequestTarget = sRequestTarget.slice(this._serviceUrl.length + 1);
}
oEntitySet = this._metadata._getEntitySetByPath(sRequestTarget);
if (oEntitySet) {
mAffectedTargets[oEntitySet.name] = true;
}
aMessages.forEach(function (oMessage) {
oMessage.getTargets().forEach(function (sTarget) {
var sParentEntity,
iSlashPos,
sTrimmedTarget;
if (!sTarget) {
return;
}
sTrimmedTarget = sTarget.replace(rEnclosingSlashes, "");
mAffectedTargets[sTrimmedTarget] = true;
iSlashPos = sTrimmedTarget.lastIndexOf("/");
if (iSlashPos > 0) {
// this may be no entity, but we keep the existing logic to avoid regressions
sParentEntity = sTrimmedTarget.slice(0, iSlashPos);
mAffectedTargets[sParentEntity] = true;
}
});
});
return mAffectedTargets;
};
/**
* This method calculates the message delta and gives it to the MessageProcessor (fires the
* messageChange-event) based on the entities belonging to this request. By using the request header
* "sap-messages" with the value <code>transientOnly</code> all existing messages are kept with the
* expectation to only receive transition messages from the back end.
*
* @param {sap.ui.core.message.Message[]} aMessages
* All messaged returned from the back-end in this request
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* Info object about the request URL. If the "request" property of "mRequestInfo" is flagged with
* "updateAggregatedMessages=true", all aggregated messages for the entities in the response are
* updated. Aggregated messages are messages of child entities of these entities which belong to
* the same business object.
* @param {map} [mGetEntities] - A map containing the entities requested from the back-end as keys
* @param {map} [mChangeEntities] - A map containing the entities changed on the back-end as keys
* @param {boolean} bSimpleMessageLifecycle
* This flag is set to false, if the used OData Model v2 supports message scopes
*/
ODataMessageParser.prototype._propagateMessages = function(aMessages, mRequestInfo, mGetEntities,
mChangeEntities, bSimpleMessageLifecycle) {
var mAffectedTargets,
sDeepPath = mRequestInfo.request.deepPath,
aKeptMessages = [],
aCanonicalPathsOfReturnedEntities,
bPrefixMatch = sDeepPath && mRequestInfo.request.updateAggregatedMessages,
bTransitionMessagesOnly = mRequestInfo.request.headers
&& mRequestInfo.request.headers["sap-messages"] === "transientOnly",
aRemovedMessages = [],
bReturnsCollection
= ODataMetadata._returnsCollection(mRequestInfo.request.functionMetadata),
bStateMessages,
iStatusCode,
bSuccess;
function isTargetMatching(oMessage, aTargets) {
return aTargets.some(function (sTarget) { return mAffectedTargets[sTarget]; })
|| bPrefixMatch && oMessage.aFullTargets.some(function (sFullTarget) {
if (bReturnsCollection) {
return aCanonicalPathsOfReturnedEntities.some(function (sKey) {
var sKeyPredicate = sKey.slice(sKey.indexOf("("));
return sFullTarget.startsWith(sDeepPath + sKeyPredicate);
});
} else {
return sFullTarget.startsWith(sDeepPath);
}
});
}
mGetEntities = mGetEntities || {};
if (bTransitionMessagesOnly) {
aKeptMessages = this._lastMessages;
bStateMessages = aMessages.some(function (oMessage) {
return !oMessage.getPersistent() && !oMessage.getTechnical();
});
if (bStateMessages) {
Log.error("Unexpected non-persistent message in response, but requested only "
+ "transition messages", undefined, sClassName);
}
} else {
mAffectedTargets = this._getAffectedTargets(aMessages, mRequestInfo, mGetEntities,
mChangeEntities);
// only the mGetEntities are relevant for function imports; mChangeEntities are used for
// DELETE and MERGE requests
aCanonicalPathsOfReturnedEntities = Object.keys(mGetEntities);
iStatusCode = mRequestInfo.response.statusCode;
bSuccess = (iStatusCode >= 200 && iStatusCode < 300);
this._lastMessages.forEach(function (oCurrentMessage) {
var aTargets = oCurrentMessage.getTargets().map(function (sTarget) {
// Note: mGetEntities and mChangeEntities contain the keys without leading or
// trailing "/", so all targets must be trimmed here
sTarget = sTarget.replace(rEnclosingSlashes, "");
// Get entity for given target (properties are not affected targets as all messages
// must be sent for affected entity)
var iPropertyPos = sTarget.lastIndexOf(")/");
if (iPropertyPos > 0) {
sTarget = sTarget.substr(0, iPropertyPos + 1);
}
return sTarget;
});
if (bSuccess || bSimpleMessageLifecycle){
if (!oCurrentMessage.getPersistent()
&& isTargetMatching(oCurrentMessage, aTargets)) {
aRemovedMessages.push(oCurrentMessage);
} else {
aKeptMessages.push(oCurrentMessage);
}
} else if (!oCurrentMessage.getPersistent() && oCurrentMessage.getTechnical()
&& isTargetMatching(oCurrentMessage, aTargets)) {
aRemovedMessages.push(oCurrentMessage);
} else {
aKeptMessages.push(oCurrentMessage);
}
});
}
this.getProcessor().fireMessageChange({
oldMessages: aRemovedMessages,
newMessages: aMessages
});
this._lastMessages = aKeptMessages.concat(aMessages);
};
/**
* Creates a <code>sap.ui.core.message.Message</code> from the given JavaScript object parsed from a
* server response. Since 1.78.0 unbound non-technical messages are supported if the message scope
* for the request is <code>BusinessObject</code>.
*
* @param {ODataMessageParser~ServerError} oMessageObject
* The object containing the message data
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* Info object about the request and the response; both properties <code>request</code> and
* <code>response</code> of <code>mRequestInfo</code> are mandatory
* @param {boolean} bIsTechnical
* Whether the given message object is a technical error (like 404 - not found)
* @return {sap.ui.core.message.Message}
* The message for the given error
*/
ODataMessageParser.prototype._createMessage = function (oMessageObject, mRequestInfo,
bIsTechnical) {
var bPersistent = oMessageObject.target && oMessageObject.target.indexOf("/#TRANSIENT#") === 0
|| oMessageObject.transient
|| oMessageObject.transition
|| bIsTechnical && this._bPersistTechnicalMessages,
oTargetInfos,
sText = typeof oMessageObject.message === "object"
? oMessageObject.message.value
: oMessageObject.message,
sType = oMessageObject["@sap.severity"] || oMessageObject.severity;
oMessageObject.transition = !!bPersistent;
oTargetInfos = this._createTargets(oMessageObject, mRequestInfo, bIsTechnical);
return new Message({
code : oMessageObject.code || "",
description : oMessageObject.description,
descriptionUrl : oMessageObject.longtext_url || "",
fullTarget : oTargetInfos.aDeepPaths,
message : sText,
persistent : !!bPersistent,
processor : this._processor,
target : oTargetInfos.aTargets,
technical : bIsTechnical,
technicalDetails : {
headers : mRequestInfo.response.headers,
statusCode : mRequestInfo.response.statusCode
},
type : mSeverity2MessageType[sType] || sType
});
};
/**
* Whether the given response is the response for a successful entity creation.
*
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* A map containing information about the current request
* @return {boolean|undefined}
* <code>true</code> if the response is for a successful creation and the response header has a
* "location" property, <code>false</code> if the response is an error response for a failed
* creation, and <code>undefined</code> otherwise.
*
* @private
*/
ODataMessageParser._isResponseForCreate = function (mRequestInfo) {
var oRequest = mRequestInfo.request,
oResponse = mRequestInfo.response;
if (oRequest.method === "POST" && oResponse.statusCode == 201
&& oResponse.headers["location"]) {
return true;
}
if (oRequest.key && oRequest.created && oResponse.statusCode >= 400) {
return false;
}
return undefined;
};
/**
* Determines the absolute target URL (relative to the service URL) from the given
* <code>sODataTarget</code> and from the given request info and calculates <code>target</code> and
* <code>deepPath</code> used for the creation of a UI5 message object.
* If the given <code>sODataTarget</code> is not absolute, it uses the location header of the
* response (in case of a successful creation of an entity), the internal entity key (in case of a
* failed creation of an entity) or the request URL to determine the <code>target</code> and
* <code>deepPath</code>.
* The <code>deepPath</code> is always reduced, that means all adjacent partner attributes have been
* removed from the target path.
* If given <code>sODataTarget</code> is for a technical transition message, or if no
* <code>sODataTarget</code> is given, the request used the message scope
* <code>BusinessObject</code> and the response is no technical error, then the <code>target</code>
* and <code>deepPath</code> are set to empty string.
*
* @param {string} sODataTarget
* The target
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* A map containing information about the current request
* @param {boolean} bIsTechnical
* Whether this is a technical error (like 404 - not found)
* @param {boolean} bODataTransition
* Whether this is a transition error
* @returns {object}
* An object with the target info for the creation of a UI5 message object with the properties
* <code>deepPath</code> and <code>target</code>
* @private
*/
ODataMessageParser.prototype._createTarget = function (sODataTarget, mRequestInfo, bIsTechnical,
bODataTransition) {
var sCanonicalTarget, bCreate, sDeepPath, iPos, sPreviousCanonicalTarget, sRequestTarget, sUrl,
mUrlData, sUrlForTargetCalculation,
oRequest = mRequestInfo.request,
oResponse = mRequestInfo.response;
if (sODataTarget === undefined
&& (!bIsTechnical && oRequest.headers["sap-message-scope"] === "BusinessObject"
|| bIsTechnical && bODataTransition)) {
return {deepPath : "", target : ""};
}
sODataTarget = sODataTarget || "";
sODataTarget = sODataTarget.startsWith("/#TRANSIENT#") ? sODataTarget.slice(12) : sODataTarget;
if (sODataTarget[0] !== "/") {
bCreate = ODataMessageParser._isResponseForCreate(mRequestInfo);
sDeepPath = oRequest.deepPath || "";
if (bCreate === true) { // successful create
// special case for 201 POST requests which create a resource;
// the target is a relative resource path segment that can be appended to the location
// response header (for POST requests that create a new entity)
sUrlForTargetCalculation = oResponse.headers["location"];
} else if (bCreate === false) { // failed create
sUrlForTargetCalculation = oRequest.key;
} else {
sUrlForTargetCalculation = mRequestInfo.url;
}
mUrlData = this._parseUrl(sUrlForTargetCalculation);
sUrl = mUrlData.url;
iPos = sUrl.indexOf(this._serviceUrl);
if (iPos > -1) {
sRequestTarget = sUrl.slice(iPos + this._serviceUrl.length);
} else { // e.g. within $batch responses
sRequestTarget = "/" + sUrl;
}
// bCreate === false might be a failed function import
if (!bCreate && oRequest.functionMetadata) {
sRequestTarget = oRequest.functionTarget;
}
// If sRequestTarget is a collection, we have to add the target without a "/". In this case
// a target would start with the specific product (like "(23)"), but the request itself
// would not have the brackets
if (sRequestTarget.slice(sRequestTarget.lastIndexOf("/")).indexOf("(") > -1
|| !this._metadata._isCollection(sRequestTarget)) {// references a single entity
sDeepPath = sODataTarget ? sDeepPath + "/" + sODataTarget : sDeepPath;
sODataTarget = sODataTarget ? sRequestTarget + "/" + sODataTarget : sRequestTarget;
} else { // references a collection or the complete $batch
sDeepPath = sDeepPath + sODataTarget;
sODataTarget = sRequestTarget + sODataTarget;
}
}
sCanonicalTarget = this._processor.resolve(sODataTarget, undefined, true);
// Multiple resolve steps are necessary for paths containing multiple navigation properties
// with to n relation, e.g. /SalesOrder(1)/toItem(2)/toSubItem(3)
while (sCanonicalTarget && sCanonicalTarget.lastIndexOf("/") > 0
&& sCanonicalTarget !== sPreviousCanonicalTarget) {
sPreviousCanonicalTarget = sCanonicalTarget;
sCanonicalTarget = this._processor.resolve(sCanonicalTarget, undefined, true)
// if canonical path cannot be determined, take the previous
|| sPreviousCanonicalTarget;
}
sODataTarget = sCanonicalTarget || sODataTarget;
return {
deepPath : this._metadata._getReducedPath(sDeepPath || sODataTarget),
target : ODataUtils._normalizeKey(sODataTarget)
};
};
/**
* Computes arrays of targets and deep paths from an OData message object for the creation of a UI5
* message object see {@link sap.ui.core.message.Message}.
*
* @param {ODataMessageParser~ServerError} oMessageObject
* The object containing the message data
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* A map containing information about the current request
* @param {boolean} bIsTechnical
* Whether this is a technical error (like 404 - not found)
* @returns {object}
* An object with the target info for the creation of a UI5 message object with the properties
* <code>aDeepPaths</code>, an array containing the deep paths and <code>aTargets</code>, an array
* containing the targets
* @private
*/
ODataMessageParser.prototype._createTargets = function(oMessageObject, mRequestInfo, bIsTechnical) {
var aDeepPaths = [],
aMessageObjectTargets = Array.isArray(oMessageObject.additionalTargets)
? [oMessageObject.target].concat(oMessageObject.additionalTargets)
: [oMessageObject.target],
oTargetInfo,
aTargets = [],
that = this;
if (oMessageObject.propertyref !== undefined && aMessageObjectTargets[0] !== undefined) {
Log.warning("Used the message's 'target' property for target calculation; the property"
+ " 'propertyref' is deprecated and must not be used together with 'target'",
mRequestInfo.url, sClassName);
} else if (aMessageObjectTargets[0] === undefined) {
aMessageObjectTargets[0] = oMessageObject.propertyref;
}
aMessageObjectTargets.forEach(function (sAdditionalTarget) {
oTargetInfo = that._createTarget(sAdditionalTarget, mRequestInfo, bIsTechnical,
oMessageObject.transition);
aDeepPaths.push(oTargetInfo.deepPath);
aTargets.push(oTargetInfo.target);
});
return {
aDeepPaths : aDeepPaths,
aTargets : aTargets
};
};
/**
* Parses the header with the set headerField and tries to extract the messages from it.
*
* @param {object} oResponse - The response object from which the headers property map will be used
* @param {ODataMessageParser~RequestInfo} mRequestInfo - Info object about the request URL
* @returns {sap.ui.core.message.Message[]} An array with messages contained in the header
*/
ODataMessageParser.prototype._parseHeader = function (oResponse, mRequestInfo) {
var i, sKey, sMessages, oServerMessage,
sField = this.getHeaderField(),
aMessages = [];
if (!oResponse.headers) {
// No header set, nothing to process
return aMessages;
}
for (sKey in oResponse.headers) {
if (sKey.toLowerCase() === sField.toLowerCase()) {
sField = sKey;
}
}
if (!oResponse.headers[sField]) {
// No header set, nothing to process
return aMessages;
}
sMessages = oResponse.headers[sField];
try {
oServerMessage = JSON.parse(sMessages);
aMessages.push(this._createMessage(oServerMessage, mRequestInfo));
if (Array.isArray(oServerMessage.details)) {
for (i = 0; i < oServerMessage.details.length; i += 1) {
aMessages.push(this._createMessage(oServerMessage.details[i], mRequestInfo));
}
}
} catch (ex) {
Log.error("The message string returned by the back-end could not be parsed: '" + ex.message + "'");
return aMessages;
}
return aMessages;
};
/**
* Parses the body of the request and tries to extract the messages from it.
*
* @param {object} oResponse - The response object from which the body property will be used
* @param {ODataMessageParser~RequestInfo} mRequestInfo - Info object about the request URL
* @returns {sap.ui.core.message.Message[]} An array with messages contained in the body
* @throws {Error} If the body cannot be parsed
*/
ODataMessageParser.prototype._parseBody = function (oResponse, mRequestInfo) {
var sContentType = getContentType(oResponse);
return (sContentType && sContentType.indexOf("xml") > -1)
? this._parseBodyXML(oResponse, mRequestInfo, sContentType)
: this._parseBodyJSON(oResponse, mRequestInfo);
};
/**
* Creates a technical generic error message and returns it in an array containing only this error
* message. The <code>description</code> of the error message is the response body.
*
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* Info object about the request and the response
* @returns {sap.ui.core.message.Message[]}
* The array with the generic error message
*/
ODataMessageParser.prototype._createGenericError = function (mRequestInfo) {
return [this._createMessage({
description : mRequestInfo.response.body,
message : sap.ui.getCore().getLibraryResourceBundle().getText("CommunicationError"),
severity : MessageType.Error,
transition : true
}, mRequestInfo, true)];
};
/**
* Gets the body messages from the given outer and inner messages. If there is a message in the
* inner messages with the same code and message as the outer message, the outer message is filtered
* out. If the request given in "mRequestInfo" has a "Content-ID" header only messages without a
* "ContentID" or with the same "ContentID" are returned.
*
* @param {object} oOuterError
* The outer error message as parsed by "_parseBodyJSON" or "_parseBodyXML"; outer message differs
* in the "message" property, in JSON it is an object like {value : "foo"} and in XML it is a
* string; "_createMessage" takes care of this difference
* @param {object[]} aInnerErrors
* The inner error messages as parsed by "_parseBodyJSON" or "_parseBodyXML"
* @param {ODataMessageParser~RequestInfo} mRequestInfo
* Info object about the request URL
* @returns {sap.ui.core.message.Message[]}
* An array with messages contained in the body
*/
ODataMessageParser.prototype._getBodyMessages = function (oOuterError, aInnerErrors, mRequestInfo) {
var sContentID = mRequestInfo.request.headers["Content-ID"],
aMessages = [],
oOuterMessage = this._createMessage(oOuterError, mRequestInfo, true),
that = this;
aInnerErrors.forEach(function (oInnerError) {
var oMessage = that._createMessage(oInnerError, mRequestInfo, true);
if (oOuterMessage && oOuterMessage.getCode() === oMessage.getCode()
&& oOuterMessage.getMessage() === oMessage.getMessage()) {
oOuterMessage = undefined;
}
if (!sContentID || !oInnerError.ContentID || sContentID === oInnerError.ContentID) {
aMessages.push(oMessage);
}
});
if (oOuterMessage) {
aMessages.unshift(oOuterMessage);
}
return aMessages;
};
/**
* Logs the given messages as an error.
*
* @param {sap.ui.core.message.Message[]} aMessages Messages to be logged
* @param {object} oRequest The request object which caused the given messages
* @param {string} sStatusCode The status code of the error response
*/
ODataMessageParser.prototype._logErrorMessages = function (aMessages, oRequest, sStatusCode) {
var sErrorDetails = aMessages.length
? JSON.stringify(aMessages.map(function (oMessage) {
return {
code : oMessage.getCode(),
message : oMessage.getMessage(),
persistent : oMessage.getPersistent(),
targets : oMessage.getTargets(),
type : oMessage.getType()
};
}))
: "Another request in the same change set failed";
Log.error("Request failed with status code " + sStatusCode + ": " + oRequest.method + " "
+ oRequest.requestUri, sErrorDetails, sClassName);
};
/**
* Parses the body of a JSON request and tries to extract the messages from it.
*
* @param {object} oResponse - The response object from which the body property will be used
* @param {ODataMessageParser~RequestInfo} mRequestInfo - Info object about the request URL
* @param {string} sContentType - The content type of the response (for the XML parser)
* @returns {sap.ui.core.message.Message[]} An array with messages contained in the body
* @throws {Error} If the body cannot be parsed
*/
ODataMessageParser.prototype._parseBodyXML = function(oResponse, mRequestInfo, sContentType) {
var oChildNode, sChildName, oError, i, m, n, oNode,
oDoc = new DOMParser().parseFromString(oResponse.body, sContentType),
aElements = getAllElements(oDoc, [ "error", "errordetail" ]),
aErrors = [];
if (!aElements.length) {
return this._createGenericError(mRequestInfo);
}
for (i = 0; i < aElements.length; i += 1) {
oNode = aElements[i];
oError = {};
// Manually set severity in case we get an error response
oError.severity = MessageType.Error;
for (n = 0; n < oNode.childNodes.length; n += 1) {
oChildNode = oNode.childNodes[n];
sChildName = oChildNode.nodeName;
if (sChildName === "errordetails" || sChildName === "details"
|| sChildName === "innererror" || sChildName === "#text") {
// Ignore known children that contain other errors
continue;
}
if (sChildName === "message" && oChildNode.hasChildNodes()
&& oChildNode.firstChild.nodeType !== window.Node.TEXT_NODE) {
// Special case for V2 error message - the message is in the child node "value"
for (m = 0; m < oChildNode.childNodes.length; m += 1) {
if (oChildNode.childNodes[m].nodeName === "value") {
oError.message = oChildNode.childNodes[m].text
|| oChildNode.childNodes[m].textContent;
}
}
} else {
oError[oChildNode.nodeName] = oChildNode.text || oChildNode.textContent;
}
}
aErrors.push(oError);
}
return this._getBodyMessages(aErrors[0], aErrors.slice(1), mRequestInfo);
};
/**
* Parses the body of a JSON request and tries to extract the messages from it.
*
* @param {object} oResponse - The response object from which the body property will be used
* @param {ODataMessageParser~RequestInfo} mRequestInfo - Info object about the request URL
* @returns {sap.ui.core.message.Message[]} An array with messages contained in the body
* @throws {Error} If the body cannot be parsed
*/
ODataMessageParser.prototype._parseBodyJSON = function(oResponse, mRequestInfo) {
var aInnerErrors, oOuterError,
oErrorResponse = JSON.parse(oResponse.body);
if (oErrorResponse.error) {
// V4 response according to OData specification or V2 response according to MS specification
// and SAP message specification
oOuterError = oErrorResponse.error;
} else {
// Actual V2 response in some tested services
oOuterError = oErrorResponse["odata.error"];
}
if (!oOuterError) {
Log.error("Error message returned by server did not contain error-field");
return this._createGenericError(mRequestInfo);
}
// Manually set severity in case we get an error response
oOuterError.severity = MessageType.Error;
// Check if more than one error has been returned from the back-end
if (Array.isArray(oOuterError.details)) {
// V4 errors
aInnerErrors = oOuterError.details;
} else if (oOuterError.innererror && Array.isArray(oOuterError.innererror.errordetails)) {
// V2 errors
aInnerErrors = oOuterError.innererror.errordetails;
} else {
// No further errors
aInnerErrors = [];
}
return this._getBodyMessages(oOuterError, aInnerErrors, mRequestInfo);
};
/**
* Parses the URL into an info map containing the url, the parameters and the has in its properties
*
* @param {string} sUrl - The URL to be stripped
* @returns {ODataMessageParser~UrlInfo} An info map about the parsed URL
* @private
*/
ODataMessageParser.prototype._parseUrl = function(sUrl) {
var mUrlData = {
url: sUrl,
parameters: {},
hash: ""
};
var iPos = -1;
iPos = sUrl.indexOf("#");
if (iPos > -1) {
mUrlData.hash = mUrlData.url.substr(iPos + 1);
mUrlData.url = mUrlData.url.substr(0, iPos);
}
iPos = sUrl.indexOf("?");
if (iPos > -1) {
var sParameters = mUrlData.url.substr(iPos + 1);
mUrlData.parameters = URI.parseQuery(sParameters);
mUrlData.url = mUrlData.url.substr(0, iPos);
}
return mUrlData;
};
/**
* Sets whether technical messages should always be treated as persistent.
*
* @param {boolean} bPersistTechnicalMessages
* Whether technical messages should always be treated as persistent
* @private
*/
ODataMessageParser.prototype._setPersistTechnicalMessages = function (bPersistTechnicalMessages) {
this._bPersistTechnicalMessages = bPersistTechnicalMessages;
};
///////////////////////////////////////// Hidden Functions /////////////////////////////////////////
/**
* Returns the content-type header of the given response, it searches in a case-insentitive way for
* the header
*
* @param {object} oResponse - The response object from which the body property will be used
* @return {string|false} Either the content-type header content or false if none is found
* @private
*/
function getContentType(oResponse) {
if (oResponse && oResponse.headers) {
for (var sHeader in oResponse.headers) {
if (sHeader.toLowerCase() === "content-type") {
return oResponse.headers[sHeader].replace(/([^;]*);.*/, "$1");
}
}
}
return false;
}
/**
* Local helper element used to determine the path of a URL relative to the server
*
* @type {HTMLAnchorElement}
*/
var oLinkElement = document.createElement("a");
/**
* Returns the URL relative to the host (i.e. the absolute path on the server) for the given URL
*
* @param {string} sUrl - The URL to be converted
* @returns {string} The server-relative URL
*/
function getRelativeServerUrl(sUrl) {
oLinkElement.href = sUrl;
return URI.parse(oLinkElement.href).path;
}
/**
* Returns all elements in the given document (or node) that match the given elementnames
*
* @param {Node} oDocument - The start node from where to search for elements
* @param {string[]} aElementNames - The names of the elements to search for
* @returns {HTMLElement[]} The matching elements
* @private
*/
function getAllElements(oDocument, aElementNames) {
var aElements = [];
var mElementNames = {};
for (var i = 0; i < aElementNames.length; i += 1) {
mElementNames[aElementNames[i]] = true;
}
var oElement = oDocument;
while (oElement) {
if (mElementNames[oElement.tagName]) {
aElements.push(oElement);
}
if (oElement.hasChildNodes()) {
oElement = oElement.firstChild;
} else {
while (!oElement.nextSibling) {
oElement = oElement.parentNode;
if (!oElement || oElement === oDocument) {
oElement = null;
break;
}
}
if (oElement) {
oElement = oElement.nextSibling;
}
}
}
return aElements;
}
//////////////////////////////////////// Overridden Methods ////////////////////////////////////////
return ODataMessageParser;
});