/*!
* SAP SE
* (c) Copyright 2009-2024 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/**
* Model and related classes like bindings for FHIR® Release 4 (R4).
*
* @name sap.fhir.model.r4
* @namespace
* @public
* @since 1.0.0
*/
// Provides class sap.fhir.model.r4.FHIRModel
sap.ui.define([
"sap/ui/model/Model",
"sap/fhir/model/r4/FHIRListBinding",
"sap/fhir/model/r4/FHIRPropertyBinding",
"sap/fhir/model/r4/FHIRContextBinding",
"sap/fhir/model/r4/FHIRTreeBinding",
"sap/fhir/model/r4/FHIRUtils",
"sap/fhir/model/r4/OperationMode",
"sap/ui/thirdparty/URI",
"sap/fhir/model/r4/lib/BindingInfo",
"sap/fhir/model/r4/lib/Sliceable",
"sap/fhir/model/r4/SubmitMode",
"sap/fhir/model/r4/lib/FHIRRequestor",
"sap/fhir/model/r4/lib/HTTPMethod",
"sap/fhir/model/r4/lib/FHIRBundle",
"sap/ui/model/ChangeReason",
"sap/fhir/model/r4/lib/FHIRUrl",
"sap/base/Log",
"sap/base/util/deepEqual",
"sap/base/util/each",
"sap/fhir/model/r4/Context",
"sap/ui/core/message/Message",
"sap/ui/core/library",
"sap/fhir/model/r4/FHIRFilterProcessor",
"sap/fhir/model/r4/FHIRFilterOperator",
"sap/fhir/model/r4/type/Url",
"sap/fhir/model/r4/type/Uuid"
], function(Model, FHIRListBinding, FHIRPropertyBinding,
FHIRContextBinding, FHIRTreeBinding, FHIRUtils, OperationMode, URI, BindingInfo, Sliceable, SubmitMode, FHIRRequestor, HTTPMethod, FHIRBundle, ChangeReason, FHIRUrl, Log, deepEqual, each, Context, Message, coreLibrary, FHIRFilterProcessor, FHIRFilterOperator, Url, Uuid) {
"use strict";
var MessageType = coreLibrary.MessageType;
/**
* Constructor for a new FHIRModel. Model implementation for FHIR R4.
*
* @class
* @classdesc The implementation of the FHIRModel
* @alias sap.fhir.model.r4.FHIRModel
* @param {string} sServiceUrl The root URL of the FHIR server to request data from e.g. http://example.com/fhir
* @param {object} mParameters The parameters
* @param {string} [mParameters.baseProfileUrl] The URL of the base profiles for all resource types. If no one is given, the model will use the FHIR default profiles located at
* http://hl7.org/fhir/StructureDefinition/ The base profile of a resource type is used to load the structure definition of a requested resource type, if no profile is maintained
* (oResource.meta.profile[0]) at the requested resource
* @param {string} [mParameters.defaultSubmitMode] The default SubmitMode for all bindings which are associated with this model
* @param {string} [mParameters.defaultFullUrlType='uuid'] The default FullUrlType if the default submit mode is either batch or transaction
* @param {object} [mParameters.defaultQueryParameters={}] The default query parameters to be passed on resource type specific requests and not resource instance specific requests (e.g /Patient?_total:accurate&_format:json). It should be of type key:value pairs. e.g. {'_total':'accurate'} -> http://hl7.org/fhir/http.html#parameters
* @param {string} [mParameters.Prefer='return=minimal'] The FHIR server won't return the changed resource by an POST/PUT request -> https://www.hl7.org/fhir/http.html#2.21.0.5.2
* @param {boolean} [mParameters.x-csrf-token=false] The model handles the csrf token between the browser and the FHIR server
* @param {object} [mParameters.filtering={}] The filtering options
* @param {boolean} [mParameters.filtering.complex=false] The default filtering type. If <code>true</code>, all search parameters would be modelled via {@link https://www.hl7.org/fhir/search_filter.html _filter}
* @param {boolean} [mParameters.search={}] The search options
* @param {boolean} [mParameters.search.secure=false] To enable RESTful search via {@link https://www.hl7.org/fhir/http.html#search POST}
* @throws {Error} If no service URL is given, if the given service URL does not end with a forward slash
* @author SAP SE
* @public
* @since 1.0.0
* @version 2.4.0
*/
var FHIRModel = Model.extend("sap.fhir.model.r4.FHIRModel", {
constructor : function(sServiceUrl, mParameters) {
Model.apply(this);
if (!sServiceUrl) {
throw new Error("Missing service root URL");
}
sServiceUrl = sServiceUrl.slice(-1) === "/" ? sServiceUrl.slice(0, -1) : sServiceUrl;
this.oServiceUrl = new URI(sServiceUrl);
this.sServiceUrl = this.oServiceUrl.query("").toString();
this._setupData();
this.aCallAfterUpdate = [];
this.sDefaultOperationMode = OperationMode.Server;
this.sBaseProfileUrl = mParameters && mParameters.baseProfileUrl ? mParameters.baseProfileUrl : "http://hl7.org/fhir/StructureDefinition/";
this._buildGroupProperties(mParameters);
this.oDefaultQueryParameters = mParameters && mParameters.defaultQueryParameters && mParameters.defaultQueryParameters instanceof Object ? mParameters.defaultQueryParameters : {};
this.bSecureSearch = mParameters && mParameters.search && mParameters.search.secure ? mParameters.search.secure : false;
this.oRequestor = new FHIRRequestor(sServiceUrl, this, mParameters && mParameters["x-csrf-token"], mParameters && mParameters.Prefer, this.oDefaultQueryParameters);
this.sDefaultSubmitMode = (mParameters && mParameters.defaultSubmitMode) ? mParameters.defaultSubmitMode : SubmitMode.Direct;
this.sDefaultFullUrlType = (mParameters && mParameters.defaultSubmitMode && mParameters.defaultSubmitMode !== SubmitMode.Direct && mParameters.defaultFullUrlType) ? mParameters.defaultFullUrlType : "uuid";
this.oDefaultUri = this.sDefaultFullUrlType === "url" ? new Url() : new Uuid();
this.iSizeLimit = 10;
if (mParameters && mParameters.filtering && mParameters.filtering.complex === true){
this.iSupportedFilterDepth = undefined;
} else {
this.iSupportedFilterDepth = 2;
}
},
metadata : {
publicMethods : []
}
});
/**
* @typedef {object} sap.fhir.model.r4.RequestParameters
* @prop {object} [urlParameters] The parameters that will be passed as query strings
* @prop {function} [successBeforeMapping] The callback function which is executed if the request was successful, in particular before the map process starts
* @prop {function} [success] The callback function which is executed if the request was successful, in particular after the map process has been finished
* @prop {function} [error] The callback function which is executed if the request failed
* @prop {object} [headers] Additional HTTP Headers which should be added to the request
* @prop {string} [groupId] Identifier for the creation of a bundle which contains several requests
* @public
* @since 1.0.0
*/
/**
* @typedef {sap.fhir.model.r4.RequestParameters} sap.fhir.model.r4.RequestParametersIntern
* @prop {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oBinding] The binding which triggered the request
* @prop {boolean} [bManualSubmit] The switch if a bundle will be manually submitted
* @prop {boolean} [forceDirectCall] Flag that the request is send directly and not in a bundle with other requests
* @private
* @since 1.0.0
*/
/**
* @typedef {object} sap.fhir.model.r4.RequestInfo
* @prop {sap.fhir.model.r4.HTTPMethod} method e.g. POST, PUT, DELETE, GET
* @prop {string} url The request goes to
* @private
* @since 1.0.0
*/
/**
* Fired, when a request was sent to the FHIR Server
*
* @event sap.fhir.model.r4.FHIRModel#requestSent
* @param {sap.ui.base.Event} oEvent
* @param {sap.ui.base.EventProvider} oEvent.getSource
* @param {object} oEvent.getParameters
* @param {sap.fhir.model.r4.lib.RequestHandle} oEvent.getParameters.requestHandle Wrapper for the jqXHR request object, binding which potentially triggered the request and the request URL
* @public
* @since 1.0.0
*/
/**
* Fired, when a request is completed
*
* @event sap.fhir.model.r4.FHIRModel#requestCompleted
* @param {sap.ui.base.Event} oEvent
* @param {sap.ui.base.EventProvider} oEvent.getSource
* @param {object} oEvent.getParameters
* @param {sap.fhir.model.r4.lib.RequestHandle} oEvent.getParameters.requestHandle Wrapper for the jqXHR request object, binding which potentially triggered the request and the request URL
* @public
* @since 1.0.0
*/
/**
* Fired, when a request failed
*
* @event sap.fhir.model.r4.FHIRModel#requestFailed
* @param {sap.ui.base.Event} oEvent
* @param {sap.ui.base.EventProvider} oEvent.getSource
* @param {object} oEvent.getParameters
* @param {sap.fhir.model.r4.lib.RequestHandle} oEvent.getParameters.requestHandle Wrapper for the jqXHR request object, binding which potentially triggered the request and the request URL
* @public
* @since 1.0.0
*/
/**
* Setups the data , server state and client changes
*
* @private
* @since 1.0.0
*/
FHIRModel.prototype._setupData = function(){
this.oData = {};
this.oDataServerState = {};
this.mChangedResources = {};
this.mOrderResources = {};
this.mResourceGroupId = {};
this.mContexts = {};
this.mMessages = {};
this.mRemovedResources = {};
};
/**
* Determines the URL of the base profiles for all resource types
*
* @public
* @returns {string} The base profile URL
* @since 1.0.0
*/
FHIRModel.prototype.getBaseProfileUrl = function() {
return this.sBaseProfileUrl;
};
/**
* Determines the root URL of the FHIR server
*
* @public
* @returns {string} The service URL
* @since 1.0.0
*/
FHIRModel.prototype.getServiceUrl = function() {
return this.sServiceUrl;
};
/**
* Creates a new list binding for the given <code>sPath</code> and optional <code>oContext</code>
*
* @param {string} sPath The binding path in the model
* @param {sap.fhir.model.r4.Context} [oContext] The context which is required as base for a relative path
* @param {sap.ui.model.Sorter[]} [aSorters] The sorters to be used initially
* @param {sap.ui.model.Filter[]} [aFilters] The filters to be used initially
* @param {object} [mParameters] Map of binding parameters which can be FHIR specific parameters or binding-specific parameters as specified below.
* @param {sap.fhir.model.r4.OperationMode} [mParameters.operationMode] The operation mode for sorting and filtering {@link sap.fhir.model.r4.OperationMode.Server} is supported. All other
* operation modes including <code>undefined</code> lead to an error if 'aSorters', 'aFilters' are given or if {@link sap.fhir.model.r4.FHIRListBinding#sort} or
* {@link sap.fhir.model.r4.FHIRListBinding#filter} are called.
* @param {string} [mParameters.url] If the list binding contains a hard coded set of entities which is identified by the given URL
* @returns {sap.fhir.model.r4.FHIRListBinding} The list binding
* @throws {Error} if an unsupported operation mode is used
* @public
* @see sap.ui.model.Model#bindList
* @since 1.0.0
*/
FHIRModel.prototype.bindList = function(sPath, oContext, aSorters, aFilters, mParameters) {
var oBinding = new FHIRListBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
return oBinding;
};
/**
* Creates a new property binding for the given <code>sPath</code>
*
* @param {string} sPath The binding path in the model
* @param {sap.fhir.model.r4.Context} [oContext] The context which is required as base for a relative path
* @param {object} [mParameters] Map of binding parameters Note: Currently no FHIR specific binding parameters are used
* @returns {sap.fhir.model.r4.FHIRPropertyBinding} The property binding
* @public
* @see sap.ui.model.Model#bindProperty
* @since 1.0.0
*/
FHIRModel.prototype.bindProperty = function(sPath, oContext, mParameters) {
var oBinding = new FHIRPropertyBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* Creates a new tree binding for the given <code>sPath</code> and optional <code>oContext</code>
*
* @param {string} sPath The binding path in the model
* @param {sap.fhir.model.r4.Context} [oContext] The context which is required as base for a relative path
* @param {sap.ui.model.Filter[]} [aFilters] The filters to be used initially
* @param {object} [mParameters] Map of binding parameters which can be FHIR specific parameters
* @param {sap.ui.model.Sorter[]} [aSorters] The sorters to be used initially
* @returns {sap.fhir.model.r4.FHIRTreeBinding} The tree binding
* @throws {Error} if an unsupported operation mode is used
* @public
* @see sap.ui.model.Model#bindTree
* @since 1.0.0
*/
FHIRModel.prototype.bindTree = function(sPath, oContext, aFilters, mParameters, aSorters) {
var oBinding = new FHIRTreeBinding(this, sPath, oContext, aFilters, mParameters, aSorters);
return oBinding;
};
/**
* Creates a new context binding for the given <code>sPath</code>, <code>oContext</code> and <code>mParameters</code>
*
* @param {string} sPath The binding path in the model
* @param {sap.fhir.model.r4.Context} [oContext] The context which is required as base for a relative path
* @param {object} [mParameters] Map of binding parameters Note: Currently no FHIR specific binding parameters are used
* @returns {sap.fhir.model.r4.FHIRContextBinding} The context binding
* @public
* @see sap.ui.model.Model#bindContext
* @since 1.0.0
*/
FHIRModel.prototype.bindContext = function(sPath, oContext, mParameters) {
var oBinding = new FHIRContextBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* Transforms the retrieved data from the FHIR service to the model structure
*
* @param {object} oData The FHIR response send by the FHIR Service
* @param {object} mResponseHeaders The HTTP headers which were sent by the server e.g. etag, etc.
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} oBundleEntry The request object which identifies the executed request, either the request handle or the bundle entry
* @param {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oBinding] The binding which triggered the request
* @param {string} sGroupId The group which triggered the mapping of response
* @private
* @since 1.0.0
*/
FHIRModel.prototype._mapFHIRResponse = function(oData, mResponseHeaders, oBundleEntry, oBinding, sGroupId) {
if (oData.entry && oData.resourceType === "Bundle") {
for (var i = 0; i < oData.entry.length; i++) {
var oResource = oData.entry[i].resource;
if (oResource && oResource.resourceType === "Bundle") {
this._mapFHIRResponse(oResource, mResponseHeaders, oBundleEntry, sGroupId);
} else if (!oResource && oData.entry[i].response) {
this._updateResourceFromFHIRResponse(oData.entry[i].response, oData.entry[i].fullUrl, oBundleEntry);
} else {
this._storeResourceInModel(oResource, oBinding, sGroupId);
}
}
} else if (oData.resourceType !== "Bundle") {
this._storeResourceInModel(oData, oBinding, sGroupId);
}
};
/**
* Stores the retrieved FHIR resource in the model depending if it is a ValueSet or another resource
*
* @param {object} oResource The FHIR resource
* @param {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oBinding] The binding which triggered the request
* @param {string} sGroupId The group which triggered the mapping of response
* @private
* @since 1.0.0
*/
FHIRModel.prototype._storeResourceInModel = function(oResource, oBinding, sGroupId) {
var aResourcePath;
if (oResource.resourceType === "ValueSet" && oResource.expansion && oResource.expansion.identifier) {
aResourcePath = [oResource.resourceType, "§" + oResource.expansion.identifier + "§"];
oResource = oResource.expansion.contains;
} else {
// generate a uuid id in case resource id is not present in the response
if (!oResource.id) {
oResource.id = FHIRUtils.uuidv4();
}
aResourcePath = [oResource.resourceType, oResource.id];
var aHistoryResourcePath = [
"$_history",
oResource.resourceType,
oResource.id
];
if (oResource.meta && oResource.meta.versionId){
aHistoryResourcePath.push(oResource.meta.versionId);
}
if (oBinding){
var oBindingInfo = this.getBindingInfo(oBinding.sPath, oBinding.oContext, oBinding.mParameters && oBinding.mParameters.unique, oResource);
aResourcePath = oBindingInfo.getBinding();
}
this._setProperty(this.mResourceGroupId, FHIRUtils.deepClone(aResourcePath), sGroupId, true);
this._setProperty(this.oData, aHistoryResourcePath, oResource, true);
}
this._setProperty(this.oData, aResourcePath, oResource, true);
};
/**
* Transforms the retrieved data from the FHIR service to FHIR resource
*
* @param {object} mResponseHeaders The HTTP headers which were sent by the server e.g. etag, etc.
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} oFHIRBundleEntry FHIR Bundle entry object
* @returns {object} FHIR resource object
* @private
* @since 1.0.0
*/
FHIRModel.prototype._getUpdatedResourceFromFHIRResponse = function(mResponseHeaders, oFHIRBundleEntry){
// remove possible slash at the beginning
if (mResponseHeaders.location && mResponseHeaders.location.charAt(0) === "/") {
mResponseHeaders.location = mResponseHeaders.location.slice(1);
}
var oBindingInfo = this.getBindingInfo("/" + mResponseHeaders.location);
var oRes;
if (oFHIRBundleEntry){
oRes = oFHIRBundleEntry.getResource();
} else {
oRes = this._getProperty(this.oData, [oBindingInfo.getResourceType(), oBindingInfo.getResourceId()]);
}
oRes.id = oBindingInfo.getResourceId();
return oRes;
};
/**
* Updates the FHIR resource from the retrieved data of the FHIR service
*
* @param {object} mResponseHeaders The HTTP headers which were sent by the server e.g. etag, etc.
* @param {string} sRequestUrl The requested FHIR resource
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} oFHIRBundleEntry FHIR Bundle entry object
* @returns {object} FHIR resource object
* @private
* @since 1.0.0
*/
FHIRModel.prototype._updateResourceFromFHIRResponse = function(mResponseHeaders, sRequestUrl, oFHIRBundleEntry){
var oRes = this._getUpdatedResourceFromFHIRResponse(mResponseHeaders, oFHIRBundleEntry);
this._setProperty(oRes, ["meta", "versionId"], mResponseHeaders.etag);
this._setProperty(oRes, ["meta", "lastUpdated"], mResponseHeaders.lastModified);
return oRes;
};
/**
* Maps a given set of <code>aBundleEntries</code> to a map of resources
*
* @param {array} aBundleEntries The set of bundle arrays
* @returns {object} The map of resources
* @private
* @since 1.0.0
*/
FHIRModel.prototype._mapBundleEntriesToResourceMap = function(aBundleEntries) {
var mResources = {};
for (var i = 0; i < aBundleEntries.length; i++) {
var oResource;
if (!aBundleEntries[i]) {
throw new Error("No response from the FHIR Service available");
}
if (!aBundleEntries[i].resource && aBundleEntries[i].response) {
oResource = this._getUpdatedResourceFromFHIRResponse(aBundleEntries[i].response);
} else {
oResource = aBundleEntries[i].resource;
}
if (oResource && oResource.resourceType == "Bundle" && oResource.entry) {
mResources = this._mapBundleEntriesToResourceMap(oResource.entry);
} else if (oResource && oResource.resourceType && oResource.id) {
this._setProperty(mResources, [oResource.resourceType, oResource.id], oResource, true);
} else {
throw new Error("No resource could be found for bundle entry: " + aBundleEntries[i]);
}
}
return mResources;
};
/**
* Maps a given FHIR resource to a map of resources
*
* @param {object} oData The FHIR resource
* @returns {object} The map of resources
* @private
* @since 1.0.0
*/
FHIRModel.prototype._mapResourceToResourceMap = function(oData) {
var mResources = {};
if (oData && oData.resourceType === "ValueSet" && oData.expansion && oData.expansion.identifier) {
this._setProperty(mResources, ["ValueSet", "§" + oData.expansion.identifier + "§"], oData.expansion.contains, true);
} else if (oData && oData.resourceType && oData.id && oData.resourceType !== "Bundle") {
this._setProperty(mResources, [oData.resourceType, oData.id], oData, true);
} else if (!oData){
throw new Error("No data could be found which should be mapped as updated resource");
}
return mResources;
};
/**
* Processes the response of the requested FHIR service. Fills the model with the retrieved data and updates all effected bindings and fires the event
*
* @param {sap.fhir.model.r4.lib.RequestHandle} oRequestHandle The request object which identifies the executed request
* @param {object} [oResponse] The FHIR response send by the FHIR Service
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} [oBundleEntry] The bundle entry of a bundle request
* @param {function} fnSuccessCallbackBeforeMapping The callback function which is executed before the map process starts
* @param {function} fnSuccessCallbackAfterMapping The callback function which is executed after the map process finished
* @param {sap.fhir.model.r4.HTTPMethod} sMethod The HTTP method which was used by the request e.g. GET, HTTPMethod.POST, etc.
* @param {object} mParameters The URL parameters which are send by the request e.g. _count, _summary
* @private
* @since 1.0.0
*/
FHIRModel.prototype._onSuccessfulRequest = function(oRequestHandle, oResponse, oBundleEntry, fnSuccessCallbackBeforeMapping, fnSuccessCallbackAfterMapping, sMethod, mParameters) {
var sRequestUrl;
var mResponseHeaders;
var sGroupId;
var oBinding;
if (oBundleEntry) {
oBinding = oBundleEntry.getRequest().getBinding();
sRequestUrl = oBundleEntry.getRequest().getUrl();
sGroupId = oRequestHandle.getBundle().getGroupId();
if (oResponse.resource && HTTPMethod.GET === sMethod){
oResponse = oResponse.resource;
} else if (HTTPMethod.DELETE !== sMethod) {
mResponseHeaders = oResponse.response;
oResponse = this._updateResourceFromFHIRResponse(mResponseHeaders, oBundleEntry.getFullUrl(), oBundleEntry);
}
} else {
sRequestUrl = oRequestHandle.getUrl();
mResponseHeaders = this.oRequestor.getResponseHeaders(oRequestHandle.getRequest());
oBinding = oRequestHandle.getBinding();
}
if (fnSuccessCallbackBeforeMapping) {
fnSuccessCallbackBeforeMapping(oResponse);
}
if (sMethod !== HTTPMethod.HEAD){
var oFHIRUrl;
if (sMethod === HTTPMethod.DELETE){
oFHIRUrl = new FHIRUrl(sRequestUrl, this.sServiceUrl);
oResponse = FHIRUtils.deepClone(this.oData[oFHIRUrl.getResourceType()][oFHIRUrl.getResourceId()]);
delete this.oData[oFHIRUrl.getResourceType()][oFHIRUrl.getResourceId()];
} else if (!oResponse){ // in case it was a direct request and the prefer header is set to minimal
oResponse = JSON.parse(oRequestHandle.getData());
oFHIRUrl = new FHIRUrl(mResponseHeaders.location, this.sServiceUrl);
oResponse.id = oFHIRUrl.getResourceId();
oResponse.meta = {};
oResponse.meta.versionId = oFHIRUrl.getHistoryVersion();
oResponse.meta.lastUpdated = mResponseHeaders["last-modified"];
this.oData[oResponse.resourceType][oResponse.id] = oResponse;
} else {
if (oBinding && oBinding.sGroupId) {
sGroupId = oBinding.sGroupId;
}
this._mapFHIRResponse(oResponse, mResponseHeaders, oBundleEntry, oBinding, sGroupId);
}
var mChangedResources = oResponse.entry ? this._mapBundleEntriesToResourceMap(oResponse.entry) : this._mapResourceToResourceMap(oResponse);
this.checkUpdate(false, mChangedResources, oBinding, sMethod);
}
if (fnSuccessCallbackAfterMapping) {
fnSuccessCallbackAfterMapping(oResponse);
}
};
/**
* Tracks the error of the requested FHIR service. Logs the error and fires the event
*
* @param {sap.fhir.model.r4.lib.RequestHandle} oRequestHandle The request handle object which identifies the executed request
* @param {object} oResponse The response body of the FHIR Server
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} oBundleEntry The FHIR Bundle Entry
* @param {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oBinding] The binding which triggered the request
* @param {function} fnErrorCallback The callback function which is executed before log process starts
* @param {string} sMethod The HTTP method which was used by the request e.g. GET, HTTPMethod.POST, etc.
* @param {Error} oError stacktrace with error message which occured in a callback
* @private
* @since 1.0.0
*/
FHIRModel.prototype._processError = function(oRequestHandle, oResponse, oBundleEntry, oBinding, fnErrorCallback, sMethod, oError) {
var oMessage = this._publishMessage(oRequestHandle, oResponse, oBundleEntry, oBinding, oError);
Log.fatal(sMethod + " " + oMessage.getDescriptionUrl() + ", Statuscode: " + oMessage.getCode() + "\nError message: " + oMessage.getMessage());
if (fnErrorCallback) {
fnErrorCallback(oMessage);
}
};
/**
* Tracks the error of the requested FHIR service. Logs the error and fires the event
*
* @param {sap.fhir.model.r4.lib.RequestHandle} oRequestHandle The request handle object which identifies the executed request
* @param {object} [oResponse] The response body of the FHIR Server
* @param {sap.fhir.model.r4.lib.FHIRBundleEntry} [oBundleEntry] The FHIR Bundle Entry
* @param {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oBinding] The binding which triggered the request
* @param {Error} [oError] stacktrace with error message which occured in a callback
* @returns {object} oMessage The message which was created
* @private
* @since 1.0.0
*/
FHIRModel.prototype._publishMessage = function(oRequestHandle, oResponse, oBundleEntry, oBinding, oError){
var mParameters;
if (oBundleEntry && oResponse){
var oErrorCode = parseInt(oResponse.response.status.substring(0,3), 10);
mParameters = {
message : oResponse.response.status.substring(4),
description : JSON.stringify(oResponse.response),
code : oErrorCode,
descriptionUrl : oBundleEntry.getRequest().getUrl(),
binding : oBinding,
additionalText : oErrorCode
};
} else {
mParameters = {
message : oRequestHandle.getRequest().statusText,
description : oRequestHandle.getRequest().responseText,
code : oRequestHandle.getRequest().status,
descriptionUrl : oRequestHandle.getUrl(),
binding : oBinding,
additionalText : oRequestHandle.getRequest().status
};
}
mParameters.type = MessageType.Error;
if (oError){
mParameters.message = oError.message;
mParameters.additionalText = oError.stack;
}
var oMessage = new Message(mParameters);
if ((!this.mMessages[oMessage.descriptionUrl] || !oBinding) && oMessage.code){
this.mMessages[oMessage.descriptionUrl] = oMessage;
this.fireMessageChange({newMessages : oMessage});
}
return oMessage;
};
/**
* Loads data from a FHIR service
*
* @param {string} sPath The path of the resource which will be requested, relative to the root URL of the FHIR server
* @param {sap.fhir.model.r4.RequestParameters | sap.fhir.model.r4.RequestParametersIntern} [mParameters] The URL parameters which are send by the request e.g. _count, _summary
* @param {string} [sMethod] The HTTP method which was used by the request e.g. GET, HTTPMethod.POST, etc.
* @param {object} [oPayload] The data which will be send in the request header
* @returns {sap.fhir.model.r4.lib.FHIRBundle | sap.fhir.model.r4.lib.RequestHandle} A request handle or a bundle.
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.loadData =
function(sPath, mParameters, sMethod, oPayload) {
sMethod = sMethod || HTTPMethod.GET;
if (!mParameters){
mParameters = {};
}
var oBinding = mParameters.binding;
var sGroupId = mParameters.groupId || (oBinding && oBinding.sGroupId);
var fnSuccess = function(oRequestHandle, oResponse, oBundleEntry) {
if (!oResponse){
oResponse = oRequestHandle.getRequest().responseJSON;
}
try {
this._onSuccessfulRequest(oRequestHandle, oResponse, oBundleEntry, mParameters.successBeforeMapping, mParameters.success, sMethod, mParameters.urlParameters);
var sUrl = oBundleEntry ? oBundleEntry.getRequest().getUrl() : oRequestHandle.getUrl();
if (this.mMessages[sUrl]){
this.fireMessageChange({oldMessages : this.mMessages[sUrl]});
delete this.mMessages[sUrl];
}
} catch (oError){
this._processError(oRequestHandle, oResponse, oBundleEntry, oBinding, mParameters.error, sMethod, oError);
}
}.bind(this);
var fnError = function(oRequestHandle, oResponse, oBundleEntry) {
this._processError(oRequestHandle, oResponse, oBundleEntry, oBinding, mParameters.error, sMethod);
}.bind(this);
var oRequestHandle = this.oRequestor._request(sMethod, sPath, mParameters.forceDirectCall, mParameters.urlParameters, sGroupId, mParameters.headers, oPayload, fnSuccess, fnError, oBinding, mParameters.manualSubmit);
return oRequestHandle;
};
/**
* Submits the client changes of the model to the FHIR service
*
* @param {string} [sGroupId] The group id to submit only a specific group, leave empty when all changes should be submitted
* @param {function} [fnSuccessCallback] The callback function which is executed with specific parameters after the changes are send successfully to the server<br>
* Batch/Transaction Submit Mode fnSuccessCallback(aFHIRResources)<br>
* Direct Mode fnSuccessCallback(oFHIRResource)
* @param {function} [fnErrorCallback] The callback function which is executed with specific parameters when the transport to the server failed<br>
* Batch/Transaction Submit Mode fnErrorCallback(oMessage, aSuccessResource, aOperationOutcome)<br>
* Direct Mode fnErrorCallback(oMessage)
* @returns {object} mRequestHandles contains all request groups and direct requests which where submitted, e.g. {"patientDetails": oFHIRBundle1, "direct": [oRequestHandle1, oRequestHandle2],
* "patientList": oFHIRBundle2}, if there are no changes undefined is returned
* @public
* @since 1.0.0
*/
FHIRModel.prototype.submitChanges =
function(sGroupId, fnSuccessCallback, fnErrorCallback) {
var aRemovedResource = this._getRemovedResourcesObject();
if (typeof sGroupId === "function") {
fnErrorCallback = fnSuccessCallback;
fnSuccessCallback = FHIRUtils.deepClone(sGroupId);
sGroupId = undefined;
}
var fnError = function (oParams) {
if (fnErrorCallback) {
fnErrorCallback(oParams);
}
};
var fnSuccess = function () {
this.resetChanges(sGroupId, true);
}.bind(this);
var mRequestHandles;
var aPromises = [];
var iTriggeredVersionRequests = 0;
var fnSubmitBundles = function () {
var oPromiseHandler = {};
var fnSuccessPromise = function (aFHIRResource) {
oPromiseHandler.resolve(aFHIRResource);
};
var fnErrorPromise = function (oRequestHandle, aFHIRResource, aFHIROperationOutcome) {
var oError = {};
// this is done since promise catch can have only one parameter
oError.requestHandle = oRequestHandle;
oError.resources = aFHIRResource;
oError.operationOutcomes = aFHIROperationOutcome;
oPromiseHandler.reject(oError);
};
for (var sRequestHandleKey in mRequestHandles) {
if (sRequestHandleKey !== "direct") {
// eslint-disable-next-line no-undef
var oPromise = new Promise(
function (resolve, reject) {
oPromiseHandler.resolve = resolve;
oPromiseHandler.reject = reject;
}
);
aPromises.push(oPromise);
oPromise.then(function (aFHIRResource) {
if (aFHIRResource.length == 0) {
aFHIRResource = aRemovedResource;
}
fnSuccessCallback(aFHIRResource);
}).catch(function (oError) {
if (fnErrorCallback && oError.requestHandle) {
if (aRemovedResource.length != 0) {
var aId = FHIRUtils.getIdFromOperationOutcome(oError.operationOutcomes);
oError.resources = FHIRUtils.filterResourcesByIds(aRemovedResource, aId);
}
var mParameters = {
message: oError.requestHandle.getRequest().statusText,
description: oError.requestHandle.getRequest().responseText,
code: oError.requestHandle.getRequest().status,
descriptionUrl: oError.requestHandle.getUrl()
};
var oMessage = new Message(mParameters);
fnErrorCallback(oMessage, oError.resources, oError.operationOutcomes);
}
});
mRequestHandles[sRequestHandleKey] = this.oRequestor.submitBundle(sRequestHandleKey, fnSuccessPromise, fnErrorPromise);
}
}
}.bind(this);
var oBindingInfo;
var fnCheckSubmitChange = function(){
var iTriggeredVersionRequestsCompleted = 0;
var aResourcePath = oBindingInfo.getResourcePathArray();
var oResourceNew = this._getProperty(this.oData, aResourcePath);
var sResourceGroupId = this._getProperty(this.mResourceGroupId, aResourcePath);
var bSubmitChanges;
if (sGroupId && sResourceGroupId === sGroupId){
bSubmitChanges = true;
} else if (!sGroupId) {
bSubmitChanges = true;
} else {
bSubmitChanges = false;
}
if (bSubmitChanges){
var oResourceOld = this._getProperty(this.oDataServerState, aResourcePath);
var oRequestInfo = this._getProperty(this.mChangedResources, aResourcePath);
if (!deepEqual(oResourceNew, oResourceOld) || (oRequestInfo && oRequestInfo.method === HTTPMethod.DELETE)) {
var mHeaders;
var fnSubmitChange = function() {
var mParameters = {
successBeforeMapping : fnSuccess,
success : fnSuccessCallback,
error : fnError,
headers : mHeaders,
groupId : sResourceGroupId,
manualSubmit : true
};
if (sGroupId && sResourceGroupId === sGroupId && this.getGroupSubmitMode(sGroupId) !== "Direct") {
mParameters.success = function () { };
mParameters.error = function () { };
}
var vRequestHandle = this.loadData(oRequestInfo.url, mParameters, oRequestInfo.method, oResourceNew);
mRequestHandles = mRequestHandles ? mRequestHandles : {};
if (vRequestHandle instanceof FHIRBundle && !mRequestHandles[vRequestHandle.getGroupId()]) {
mRequestHandles[vRequestHandle.getGroupId()] = {};
} else if (!mRequestHandles.direct) {
mRequestHandles.direct = [];
mRequestHandles.direct.push(vRequestHandle);
} else {
mRequestHandles.direct.push(vRequestHandle);
}
}.bind(this);
var fnVersionReadSuccess = function(sETag){
iTriggeredVersionRequestsCompleted++;
mHeaders = {
"If-Match" : sETag
};
fnSubmitChange();
if (iTriggeredVersionRequests === iTriggeredVersionRequestsCompleted){
fnSubmitBundles();
}
};
if (oRequestInfo.method === HTTPMethod.PUT) {
var sETag = oBindingInfo.getETag();
if (!sETag){
this.readLatestVersionOfResource(oBindingInfo.getResourceServerPath(), fnVersionReadSuccess);
iTriggeredVersionRequests++;
} else {
mHeaders = {
"If-Match" : sETag
};
fnSubmitChange();
}
} else {
fnSubmitChange();
}
}
}
}.bind(this);
for ( var vType in this.mChangedResources) {
for (var vId in this.mChangedResources[vType]){
if (vType === "$_history"){
for (var vOriginId in this.mChangedResources[vType][vId]){
for (var sVersion in this.mChangedResources[vType][vId][vOriginId]){
oBindingInfo = this.getBindingInfo("/" + vType + "/" + vId + "/" + vOriginId + "/" + sVersion);
fnCheckSubmitChange();
}
}
} else {
oBindingInfo = this.getBindingInfo("/" + vType + "/" + vId);
fnCheckSubmitChange();
}
}
}
if (iTriggeredVersionRequests === 0){
fnSubmitBundles();
}
return mRequestHandles;
};
/**
* Retrieves an array of resources that have been removed from the FHIR model.
* Iterates through the removed resources,
* retrieves corresponding resources from the model, and returns them.
* @returns {Array<object>} An array containing the removed resources.
* @private
* @since 2.4.0
*/
FHIRModel.prototype._getRemovedResourcesObject = function () {
var aResource = [];
for (var sType in this.mRemovedResources) {
if (this.mRemovedResources.hasOwnProperty(sType)) {
var oRemovedResource = this.mRemovedResources[sType];
for (var sKey in oRemovedResource) {
var oResource = this.getProperty("/" + oRemovedResource[sKey]);
if (oResource) {
aResource.push(oResource);
}
}
}
}
return aResource;
};
/**
* Checks if an update for the existing bindings is necessary due to the <code>mChangedResources</code>
*
* @param {boolean} [bForceUpdate] Force update of bindings
* @param {object} [mChangedResources] The object containing the changed resources
* @param {sap.fhir.model.r4.FHIRContextBinding | sap.fhir.model.r4.FHIRListBinding | sap.fhir.model.r4.FHIRTreeBinding} [oTriggerBinding] The binding which triggered the check update
* @param {sap.fhir.model.r4.HTTPMethod} [sMethod] The http method which triggered the checkupdate()
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.checkUpdate = function(bForceUpdate, mChangedResources, oTriggerBinding, sMethod) {
var aBindings = this.aBindings.slice(0);
each(aBindings, function(iIndex, oBinding) {
oBinding.checkUpdate(bForceUpdate, mChangedResources, sMethod);
});
this._processAfterUpdate();
};
/**
* Registers a function which shall be executed after the update of the model has been performed
*
* @param {function} fnAfterUpdate The function which shall be executed after the update of the model has been performed
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.attachAfterUpdate = function(fnAfterUpdate) {
this.aCallAfterUpdate.push(fnAfterUpdate);
};
/**
* Executes all registered after update functions
*
* @private
* @since 1.0.0
*/
FHIRModel.prototype._processAfterUpdate = function() {
var aCallAfterUpdate = this.aCallAfterUpdate;
this.aCallAfterUpdate = [];
for (var i = 0; i < aCallAfterUpdate.length; i++) {
aCallAfterUpdate[i]();
}
};
/**
* Refreshes the model and all associated bindings
*
* @see sap.ui.model.Model#refresh
* @see sap.fhir.model.r4.FHIRContextBinding#refresh
* @see sap.fhir.model.r4.FHIRListBinding#refresh
* @see sap.fhir.model.r4.FHIRTreeBinding#refresh
* @public
* @since 1.0.0
*/
FHIRModel.prototype.refresh = function() {
this._setupData();
for (var i = 0; i < this.aBindings.length; i++){
this.aBindings[i].refresh(ChangeReason.Refresh);
}
};
/**
* Determines the value for the property in the model based on the given <code>sPath</code> and <code>oContext</code> and <code>oDataExt</code>
*
* @param {string} [sPath] The path to the desired property
* @param {sap.fhir.model.r4.Context} [oContext] The binding context
* @param {object} [oDataExt] The data object containing the desired property
* @returns {object} the value behind the path
* @public
* @since 1.0.0
*/
FHIRModel.prototype.getProperty = function(sPath, oContext, oDataExt) {
var oData = this.oData;
if (oDataExt) {
oData = oDataExt;
}
var oBinding = this.getBindingInfo(sPath, oContext);
if (oBinding) {
return this._getProperty(oData, oBinding.getBinding());
}
return undefined;
};
/**
* Determines the value for the property in the given <code>oObject</code> based on the given <code>sPath</code>
*
* @param {object} [vObject] The data object containing the desired property
* @param {string[]} aPath The path to the desired property e.g ["Patient", "132627", "gender"]
* @returns {any} the value behind the path
* @private
* @since 1.0.0
*/
FHIRModel.prototype._getProperty = function(vObject, aPath) {
// deepClone is necessary here, because the original aPath value should not be overriden by the following actions
aPath = FHIRUtils.deepClone(aPath);
if (!vObject) { // property not existing
return undefined;
}
var aReferencePath;
var mSliceExpressions;
if (aPath.length === 1) {
if (Sliceable.containsSliceable(aPath[0])){
mSliceExpressions = Sliceable.getSliceables(aPath[0]);
return this._findMatchingSlice(vObject, mSliceExpressions);
} else {
return vObject[aPath[0]];
}
}
var oNextObject;
var sNextProperty = aPath.shift();
if (sNextProperty === "reference" && vObject.reference && typeof vObject.reference === "string") {
aReferencePath = FHIRUtils.splitPath(vObject.reference);
oNextObject = this.oData[aReferencePath[0]][aReferencePath[1]];
} else if (Sliceable.containsSliceable(sNextProperty)) {
mSliceExpressions = Sliceable.getSliceables(sNextProperty);
oNextObject = this._findMatchingSlice(vObject, mSliceExpressions, false);
var aKeys = oNextObject && Object.keys(oNextObject);
if (aKeys && !isNaN(aKeys[0])){
sNextProperty = aPath.shift();
oNextObject = vObject[sNextProperty];
}
} else {
oNextObject = vObject[sNextProperty];
}
return this._getProperty(oNextObject, aPath);
};
/**
* Handles the client changes info objects
*
* @param {sap.fhir.model.r4.lib.BindingInfo} oBindingInfo The binding info containing path and context
* @param {string} [sGroupId] The group id for the changed resource
* @private
* @since 1.0.0
*/
FHIRModel.prototype._handleClientChanges = function(oBindingInfo, sGroupId){
if (!sGroupId){
sGroupId = oBindingInfo.getGroupId();
}
var aResPath = oBindingInfo.getResourcePathArray();
var vServerValue = this._getProperty(this.oDataServerState, aResPath);
var oRequestInfo = this._getProperty(this.mChangedResources, aResPath);
var oResource = this._getProperty(this.oData, aResPath);
if (!oRequestInfo && oResource) {
oRequestInfo = this._createRequestInfo(HTTPMethod.PUT, oBindingInfo.getResourceServerPath());
this._setProperty(this.mChangedResources, FHIRUtils.deepClone(aResPath), oRequestInfo, true);
} else if (!oRequestInfo) {
oRequestInfo = this._createRequestInfo(HTTPMethod.POST, oBindingInfo.getResourceType());
this._setProperty(this.mChangedResources, FHIRUtils.deepClone(aResPath), oRequestInfo, true);
}
if (sGroupId) {
this._setProperty(this.mResourceGroupId, FHIRUtils.deepClone(aResPath), sGroupId, true);
}
if (!this._isServerStateUpToDate(vServerValue, oResource, oRequestInfo.method)) {
this._setProperty(this.oDataServerState, aResPath, FHIRUtils.deepClone(oResource), true);
}
};
/**
* Sets a new value for the property on the the given <code>sPath</code> in the model
*
* @param {string} sPath The path of the property to set
* @param {any} vValue The value to set the property to
* @param {sap.fhir.model.r4.Context} [oContext] The context which will be used to set the property
* @param {sap.fhir.model.r4.PropertyBinding} oBinding That the checkupdate method doesn't run in not associated bindings less round trips
* @public
* @since 1.0.0
*/
FHIRModel.prototype.setProperty = function(sPath, vValue, oContext, oBinding) {
var oBindingInfo = this.getBindingInfo(sPath, oContext);
this._handleClientChanges(oBindingInfo);
this._setProperty(this.oData, oBindingInfo.getBinding(), vValue, undefined, oBindingInfo.getGroupId());
var aResPath = oBindingInfo.getResourcePathArray();
var vServerValue = this._getProperty(this.oDataServerState, aResPath);
var oRequestInfo = this._getProperty(this.mChangedResources, aResPath);
var oResource = this._getProperty(this.oData, aResPath);
// special handling when the server data and the client changed data is the same
if (oRequestInfo && oRequestInfo.method === HTTPMethod.PUT && deepEqual(vServerValue, oResource)) {
delete this.mChangedResources[aResPath[0]][aResPath[1]];
} else {
this.mChangedResources.path = { lastUpdated: oBindingInfo.getAbsolutePath() };
}
this.checkUpdate(false, this.mChangedResources, oBinding);
};
/**
* Sets a new value for the property on the given <code>oObject</code> on the given <code>iIndex</code> in the binding <code>aPath</code>
*
* @param {object} oObject The object which should get a new property value
* @param {string[]} aPath The binding path e.g. ["Patient", "123", "gender"]
* @param {any} vValue The new property value
* @param {boolean} [bForceResourceCreation] Wether it's definitely a resource object which shall be created
* @param {string} [sGroupId] The group id for the changed resource
* @param {number} [iIndex] The index of the property in the <code>aPath</code> which should get a new value
* @private
* @since 1.0.0
*/
FHIRModel.prototype._setProperty = function(oObject, aPath, vValue, bForceResourceCreation, sGroupId, iIndex) {
if (iIndex === undefined){
iIndex = 0;
}
var oNextObject;
var aReferencePath;
var sProperty = aPath[iIndex];
var sNextProperty;
if (sProperty === "reference" && aPath.length - 1 !== iIndex){
aReferencePath = FHIRUtils.splitPath(oObject[sProperty]);
var oBindingInfo = this.getBindingInfo("/" + oObject[sProperty]);
this._handleClientChanges(oBindingInfo, sGroupId);
oNextObject = this.oData[aReferencePath[0]][aReferencePath[1]];
} else if (aPath.length - 1 === iIndex) {
if (vValue) {
oObject[sProperty] = vValue;
} else {
delete oObject[sProperty];
}
return;
} else if (Sliceable.containsSliceable(sProperty)) {
sNextProperty = aPath[iIndex + 1];
if (!isNaN(sNextProperty)){
iIndex++;
oNextObject = oObject[sNextProperty];
} else {
var mSliceExpressions = Sliceable.getSliceables(sProperty);
oNextObject = this._findMatchingSlice(oObject, mSliceExpressions);
if (!oNextObject) {
oNextObject = {};
for (var sKey in mSliceExpressions) {
var vFilter = mSliceExpressions[sKey];
if (vFilter.aFilters){
for (var i = 0; i < vFilter.aFilters.length; i++){
this._setProperty(oNextObject, FHIRUtils.splitPath(vFilter.aFilters[i].sPath), vFilter.aFilters[i].oValue1);
}
} else {
this._setProperty(oNextObject, FHIRUtils.splitPath(vFilter.sPath), vFilter.oValue1);
}
}
oObject.push(oNextObject);
}
}
} else if (!oObject.hasOwnProperty(sProperty)) {
sNextProperty = aPath[iIndex + 1];
if (bForceResourceCreation || (isNaN(sNextProperty) && !Sliceable.containsSliceable(sNextProperty))) {
oObject[sProperty] = {};
} else {
oObject[sProperty] = [];
}
}
this._setProperty(oNextObject || oObject[sProperty], aPath, vValue, bForceResourceCreation, sGroupId, iIndex + 1);
};
/**
* Sets an object in a given resource map, by default it's the changed resource map
*
* @param {sap.fhir.model.r4.HTTPMethod} sMethod e.g. POST, PUT, DELETE, GET
* @param {string} sUrl The request goes to
* @returns {sap.fhir.model.r4.RequestInfo} oRequestInfo
* @private
* @since 1.0.0
*/
FHIRModel.prototype._createRequestInfo = function(sMethod, sUrl) {
return {
method : sMethod,
url : sUrl
};
};
/**
* Resolves the absolute path by a given context with potential parent contexts and the path of the property
*
* @param {string} sPath The path inside the model
* @param {sap.fhir.model.r4.Context}[oContext] The context which resolves a relative <code>sPath</code>
* @returns {string} The absolute path to a model property
* @private
* @since 1.0.0
*/
FHIRModel.prototype._resolvePath = function(sPath, oContext){
if (oContext && (!sPath || sPath && !sPath.startsWith("/"))){
var sContextPath = oContext.sPath;
var oParentContext = oContext.getBinding().getContext();
var sRelativePath = sPath ? "/" + sPath : "";
return this._resolvePath(sContextPath, oParentContext) + sRelativePath;
} else if (sPath && sPath.startsWith("/")){
return sPath;
} else {
return undefined;
}
};
/**
* Creates the binding information based on the given <code>sPath</code> and <code>oContext</code>
*
* @param {string} sPath The path inside the model
* @param {sap.fhir.model.r4.Context}[oContext] The context which resolves a relative <code>sPath</code>
* @param {string} [bUnique] Other unique identifier property path than logical id of FHIR resource
* @param {object} [oResource] The FHIR resource
* @returns {sap.fhir.model.r4.lib.BindingInfo} The binding info to the given sPath and context
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.getBindingInfo = function(sPath, oContext, bUnique, oResource) {
var sCompletePath = this._resolvePath(sPath, oContext);
if (sCompletePath) {
var aSplittedPath = FHIRUtils.splitPath(sCompletePath);
var aResPath;
var sId;
var sResType;
var sResPath;
var sRelPath;
var sVersion;
var sCompletePathChange;
var sRequestablePath;
var sResourceServerPath;
var sETag;
var sGroupId = oContext && oContext.sGroupId;
var sOperation = "";
if (sCompletePath.indexOf("_history") > -1 || bUnique){
if (oResource){
sResType = oResource.resourceType;
sId = oResource.id;
sVersion = this._getProperty(oResource, ["meta", "versionId"]);
} else if (sCompletePath.startsWith("/$")){
sResType = aSplittedPath[2];
sId = aSplittedPath[3];
sVersion = aSplittedPath[4];
} else {
sResType = aSplittedPath[1];
sId = aSplittedPath[2];
sVersion = aSplittedPath[4];
}
if (sVersion){
sVersion = "/" + sVersion;
} else {
sVersion = "";
}
sRelPath = aSplittedPath.slice(5).join("/");
sRequestablePath = "/" + sResType + "/" + sId + "/_history" + sVersion;
sResPath = "/$_history" + "/" + sResType + "/" + sId + sVersion;
if (sRelPath){
sCompletePathChange = sResPath + "/" + sRelPath;
} else {
sCompletePathChange = sResPath;
}
} else if (oResource) {
sResType = oResource.resourceType;
sId = oResource.id;
sResPath = "/" + sResType + "/" + sId;
sCompletePathChange = sResPath;
} else {
sOperation = this.determineOperation(sCompletePath);
sResType = aSplittedPath[1];
if (aSplittedPath[2] && aSplittedPath[2] !== sOperation){
sId = aSplittedPath[2];
sResPath = "/" + aSplittedPath[1] + "/" + aSplittedPath[2];
sRelPath = aSplittedPath.slice(3).join("/");
}
if (sOperation){
sCompletePathChange = sCompletePath.replace(sOperation, "");
sRelPath = sRelPath.replace(sOperation.substring(1) + "/", "");
}
}
if (sCompletePathChange) {
aSplittedPath = FHIRUtils.splitPath(sCompletePathChange);
sCompletePath = sCompletePathChange;
}
if (!sRequestablePath){
sRequestablePath = (sResPath || sResType || "") + sOperation;
}
if (sResPath){
aResPath = FHIRUtils.splitPath(sResPath).slice(1);
}
if (sResType && sId){
sResourceServerPath = "/" + sResType + "/" + sId;
sETag = this._getProperty(this.oData, [
sResType,
sId,
"meta",
"versionId"
]);
if (sETag) {
sETag = "W/\"" + sETag + "\"";
}
}
return new BindingInfo(sId, sResType, sResPath, sRelPath, sCompletePath, aSplittedPath.slice(1), sGroupId, sRequestablePath, aResPath, sResourceServerPath, sETag);
}
return undefined;
};
/**
* Determines if there is an operation in the given string
*
* @param {string} sPath The path request path
* @returns {string} Empty string if no operation is found or \"$operation\"
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.determineOperation = function(sPath){
var sOperation = "";
var iIndexOperation = sPath.indexOf("$");
if (iIndexOperation > -1 && iIndexOperation !== 1){
var sRelPath = sPath.substring(iIndexOperation);
var iIndexNextSlash = sRelPath.indexOf("/");
if (iIndexNextSlash > -1){
sOperation = "/" + sRelPath.substring(0, iIndexNextSlash);
} else {
sOperation = "/" + sRelPath;
}
}
return sOperation;
};
/**
* Determines the property which contains a sliceable object in the given <code>vObject</code> based on the given <code>mSliceables</code>
*
* @param {object} vObject Either an array or resource when revinclude is in slice
* @param {sap.fhir.model.r4.lib.SliceableMap} mSliceables The array of sliceable objects
* @param {boolean} bIsEnd If the slice is not at the end of the absolute path
* @returns {object} The property of the given<code>vObject</code> which contains a sliceable object
* @private
* @since 1.0.0
*/
FHIRModel.prototype._findMatchingSlice = function(vObject, mSliceables, bIsEnd) {
var mEntries = {};
var oEntry;
var fnGetValue = function (oObject, sProperty){
return this._getProperty(oObject, FHIRUtils.splitPath(sProperty));
}.bind(this);
for (var sKey in mSliceables) {
if (mSliceables[sKey].sPath && mSliceables[sKey].sPath.startsWith("revreference/")){
var sResourceType = mSliceables[sKey].sPath.substring(13);
if (bIsEnd !== false){
var aPath = FHIRUtils.splitPath(mSliceables[sKey].oValue1);
for (var sResourceId in this.oData[sResourceType]){
var sResource = this._getProperty(this.oData[sResourceType][sResourceId], aPath);
if (sResource === (vObject.resourceType + "/" + vObject.id)){
mEntries[sResourceType + "/" + sResourceId] = this.oData[sResourceType][sResourceId];
}
}
return mEntries;
} else {
return this.oData;
}
} else {
for (var i = 0; i < vObject.length; i++) {
oEntry = vObject[i];
if (FHIRFilterProcessor._evaluateFilter(mSliceables[sKey], oEntry, fnGetValue)){
mEntries[i] = oEntry;
}
}
}
}
var aKeys = Object.keys(mEntries);
mEntries = aKeys.length === 0 ? undefined : mEntries;
return aKeys.length === 1 ? mEntries[aKeys[0]] : mEntries;
};
/**
* Determines if there are any pending changes for the given <code>sResourceType</code>
*
* @param {string} sResourceType The FHIR resource type
* @returns {boolean} true there are changes for the given resource type.
* @public
* @since 1.0.0
*/
FHIRModel.prototype.hasResourceTypePendingChanges = function(sResourceType) {
return this.mChangedResources[sResourceType] !== undefined && Object.keys(this.mChangedResources[sResourceType]).length > 0;
};
/**
* Destroys this model
*
* @see sap.ui.model.Model#destroy
* @public
* @override
* @since 1.0.0
*/
FHIRModel.prototype.destroy = function() {
this.oRequestor.destroy();
this.aCallAfterUpdate = [];
this.mChangedResources = {};
Model.prototype.destroy.apply(this, arguments);
};
/**
* Returns a group property value.
*
* @param {string} sGroupId The group id
* @param {string} sPropertyName The group property in question
* @returns {any} The group property value
* @throws {Error} If the name of the group property is not 'submit' or 'uri'
* @protected
* @see sap.ui.model.odata.v4.ODataModel#constructor
* @since 1.0.0
*/
FHIRModel.prototype.getGroupProperty = function(sGroupId, sPropertyName) {
switch (sPropertyName) {
case "submit":
return this.getGroupSubmitMode(sGroupId);
case "uri":
return this.getGroupUri(sGroupId);
default:
throw new Error("Unsupported group property: " + sPropertyName);
}
};
/**
* Determines the submit mode for the given <code>sGroupId</code>. If no submit mode is defined in the group properties or there are no group properties at all for the given
* <code>sGroupId</code> the default submit mode of the model is returned
*
* @param {string} sGroupId The group id
* @returns {sap.fhir.model.r4.SubmitMode} the mode for the given group
* @protected
* @since 1.0.0
*/
FHIRModel.prototype.getGroupSubmitMode = function(sGroupId) {
return (this.mGroupProperties && this.mGroupProperties[sGroupId] && this.mGroupProperties[sGroupId].submit) || this.sDefaultSubmitMode;
};
/**
* Determines the fullUrl type mode for the given <code>sGroupId</code>. If no submit mode is defined in the group properties or there are no group properties at all for the given
* <code>sGroupId</code> the default URI is returned
*
* @param {string} sGroupId The group id
* @returns {sap.fhir.model.r4.type.Uri} the URI Object
* @protected
* @since 1.1.0
*/
FHIRModel.prototype.getGroupUri = function(sGroupId) {
var oGroupUri = this.oDefaultUri;
if (this.mGroupProperties && this.mGroupProperties[sGroupId] && this.mGroupProperties[sGroupId].fullUrlType === "url"){
oGroupUri = new Url();
}
return oGroupUri;
};
/**
* Sets the group properties of the model by the given <code>mParameters</code>
*
* @param {object} mParameters The parameters
* @throws {Error} if parameters are not valid
* @private
* @since 1.0.0
*/
FHIRModel.prototype._buildGroupProperties =
function(mParameters) {
if (mParameters) {
var oGroupProperties;
for ( var sGroupId in mParameters.groupProperties) {
oGroupProperties = mParameters.groupProperties[sGroupId];
if (typeof oGroupProperties !== "object") {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. The properties must be of type object, found \"" + oGroupProperties + "\"");
} else if (Object.keys(oGroupProperties).length === 2 && (!oGroupProperties.submit || !oGroupProperties.fullUrlType)) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. Only the property \"submit\" and \"fullUrlType\" is allowed and has to be set, found \"" + JSON.stringify(oGroupProperties)
+ "\"");
} else if (Object.keys(oGroupProperties).length === 1 && !oGroupProperties.submit && oGroupProperties.fullUrlType) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. The property \"fullUrlType\" is allowed only when submit property is present, found \"" + JSON.stringify(oGroupProperties)
+ "\"");
} else if (Object.keys(oGroupProperties).length === 1 && !oGroupProperties.submit) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. Only the property \"submit\" is allowed and has to be set, found \"" + JSON.stringify(oGroupProperties)
+ "\"");
} else if (oGroupProperties.submit && !(oGroupProperties.submit in SubmitMode)) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. The value of property \"submit\" must be of type sap.fhir.model.r4.SubmitMode, found: \""
+ oGroupProperties.submit + "\"");
} else if (oGroupProperties.fullUrlType && (oGroupProperties.fullUrlType !== "uuid" && oGroupProperties.fullUrlType !== "url")) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. The value of property \"fullUrlType\" must be either uuid or url, found: \""
+ oGroupProperties.fullUrlType + "\"");
} else if (oGroupProperties.submit && (oGroupProperties.submit !== SubmitMode.Batch && oGroupProperties.submit !== SubmitMode.Transaction) && oGroupProperties.fullUrlType) {
throw new Error("Group \"" + sGroupId + "\" has invalid properties. The value of property \"fullUrlType\" is applicable only for batch and transaction submit modes, found: \""
+ oGroupProperties.submit + "\"");
}
}
this.mGroupProperties = mParameters.groupProperties;
} else {
Log.info("no parameters are defined to build group properties");
}
};
/**
* Triggers a <code>GET</code> request to the FHIR server that was specified in the model constructor. The data will be stored in the model
*
* @param {string} sPath A string containing the path to the data which should be retrieved. The path is concatenated to the service URL which was specified in the model constructor.
* @param {sap.fhir.model.r4.RequestParameters} [mParameters] The additional request parameters
* @returns {sap.fhir.model.r4.lib.RequestHandle} A request handle.
* @public
* @since 1.0.0
*/
FHIRModel.prototype.sendGetRequest = function(sPath, mParameters) {
return this.loadData(sPath, mParameters);
};
/**
* Triggers a <code>POST</code> request to the FHIR server that was specified in the model constructor. The data will be stored in the model
*
* @param {string} sPath A string containing the path to the data which should be retrieved. The path is concatenated to the service URL which was specified in the model constructor.
* @param {object} [oPayload] The request body
* @param {sap.fhir.model.r4.RequestParameters} [mParameters] The additional request parameters
* @returns {sap.fhir.model.r4.lib.RequestHandle} A request handle.
* @public
* @since 1.0.0
*/
FHIRModel.prototype.sendPostRequest = function(sPath, oPayload, mParameters) {
var bBundleType = oPayload && oPayload.type && (oPayload.type == "batch" || oPayload.type == "transaction") ? true : false;
if (bBundleType) {
mParameters.forceDirectCall = true;
}
return this.loadData(sPath, mParameters, HTTPMethod.POST, oPayload);
};
/**
* Triggers a <code>HEAD</code> request to the FHIR server that was specified in the model constructor. If HEAD isn't available it sends a GET request and updates the resource data in the model implicitly
*
* @param {string} sPath A string containing the path to the resource which should be requested.
* @param {function} [fnSuccess] The callback function which is executed after the version read was successfully
* @public
* @since 1.0.0
*/
FHIRModel.prototype.readLatestVersionOfResource = function(sPath, fnSuccess) {
var oRequestHandle;
var fnExtractVersion = function (oData) {
var mHeaders = this.oRequestor.getResponseHeaders(oRequestHandle.getRequest());
var sETagHeader = mHeaders ? mHeaders["etag"] : undefined;
var sLocationHeader = mHeaders ? mHeaders["location"] || mHeaders["content-location"] : undefined;
var oFHIRUrl = sLocationHeader ? new FHIRUrl(sLocationHeader, this.sServiceUrl) : undefined;
var sETag;
if (sETagHeader) {
sETag = sETagHeader;
} else if (oFHIRUrl && oFHIRUrl.getHistoryVersion()) {
sETag = "W/\"" + oFHIRUrl.getHistoryVersion() + "\"";
} else if (oData && oData.meta && oData.meta.versionId) {
sETag = "W/\"" + oData.meta.versionId + "\"";
}
fnSuccess(sETag);
}.bind(this);
var mParameters = {
success: fnExtractVersion,
error: function () {
oRequestHandle.getRequest().always(function () {
mParameters = {
success: fnExtractVersion
};
oRequestHandle = this.loadData(sPath, mParameters, HTTPMethod.GET);
}.bind(this));
}.bind(this)
};
oRequestHandle = this.loadData(sPath, mParameters, HTTPMethod.HEAD);
};
/**
* Creates a new resource based on the given <code>sResourceType</code> with given <code>oData</code>. Note: The resource will be created only on the client side, to push the created resource
* to the server {@link sap.fhir.model.r4.FHIRModel#submitChanges} has to be called afterwards
*
* @param {string} sResourceType The resource type
* @param {object} [oData] The data of the resource
* @param {string} [sGroupId] The group where the resource should belongs to
* @returns {string} The uuidv4 of the created resource
* @public
* @since 1.0.0
*/
FHIRModel.prototype.create = function(sResourceType, oData, sGroupId) {
var sResourceId = FHIRUtils.uuidv4();
var sResourcePath = "/" + sResourceType + "/" + sResourceId;
if (!this.mOrderResources[sResourceType]){
this.mOrderResources[sResourceType] = [sResourcePath.substring(1)];
} else {
this.mOrderResources[sResourceType].unshift(sResourcePath.substring(1));
}
if (!oData){
oData = { resourceType : sResourceType , id : sResourceId};
} else {
oData.resourceType = sResourceType;
oData.id = sResourceId;
}
this.setProperty(sResourcePath, oData);
if (sGroupId){
this._setProperty(this.mResourceGroupId, [sResourceType , sResourceId], sGroupId, true);
}
return sResourceId;
};
/**
* Mark resources for the DELETE request which have to be submitted to the server with the submitChanges() method or delete client changes
*
* @param {string[]} aResources the resources which shall be deleted, e.g. ["/Patient/123", "/Organization/XYZ"]
* @param {function} [fnPreProcess] to preprocess the objects of the given aResources
* @param {string} [sGroupId] The group where the resource should belongs to
* @public
* @since 1.0.0
*/
FHIRModel.prototype.remove = function (aResources, fnPreProcess, sGroupId) {
for (var i = 0; i < aResources.length; i++) {
var sResourcePath = fnPreProcess ? fnPreProcess(aResources[i]) : aResources[i];
var oBindingInfo = this.getBindingInfo(sResourcePath);
var aResPath = oBindingInfo.getResourcePathArray();
var oRequestInfo = this._getProperty(this.mChangedResources, aResPath);
var sResourceGroupId = this._getProperty(this.mResourceGroupId, aResPath);
if (oRequestInfo && oRequestInfo.method == HTTPMethod.POST) {
this._setProperty(this.oData, FHIRUtils.deepClone(aResPath));
this._setProperty(this.mResourceGroupId, FHIRUtils.deepClone(aResPath));
this._setProperty(this.mChangedResources, FHIRUtils.deepClone(aResPath));
this._removeFromOrderResources(oBindingInfo);
this.checkUpdate(true);
} else {
oRequestInfo = this._createRequestInfo(HTTPMethod.DELETE, oBindingInfo.getResourceServerPath());
this._setProperty(this.mChangedResources, FHIRUtils.deepClone(aResPath), oRequestInfo, true, sResourceGroupId && sResourceGroupId === sGroupId ? sGroupId : undefined);
this._addToRemovedResources(oBindingInfo, sResourcePath);
this.checkUpdate(true, this.mChangedResources, oBindingInfo, HTTPMethod.DELETE);
}
}
};
/**
* Adds the resource path to the removed resources map
*
* @param {sap.fhir.model.r4.lib.BindingInfo} oBindingInfo The binding info object
* @param {string} [sResourcePath] The resource path of the removed resource
* @private
*/
FHIRModel.prototype._addToRemovedResources = function (oBindingInfo, sResourcePath) {
if (!this.mRemovedResources[oBindingInfo.getResourceType()]) {
this.mRemovedResources[oBindingInfo.getResourceType()] = [sResourcePath.substring(1)];
} else {
this.mRemovedResources[oBindingInfo.getResourceType()].unshift(sResourcePath.substring(1));
}
};
/**
* Resets the model to the state when the model was synchronized with the server for the last time.
* Resetting means newly created resources are removed and changed resources are rolled backed to the earlier state.
*
* @param {string} [sGroupId] The groupId which identifies the changes of a specific group
* @param {boolean} [bAvoidUpdate] If true then checkupdate() won't be called, because it will anyway get called in furthers steps
* @public
* @since 1.0.0
*/
FHIRModel.prototype.resetChanges = function(sGroupId, bAvoidUpdate) {
var oBindingInfo;
var fnResetResource = function(){
var aResPath = oBindingInfo.getResourcePathArray();
var sResGroupId = this._getProperty(this.mResourceGroupId, aResPath);
var fnDoReset = function(){
var oRequestInfo = this._getProperty(this.mChangedResources, aResPath);
if (oRequestInfo.method === HTTPMethod.PUT){
var oResourceServerState = this._getProperty(this.oDataServerState, aResPath);
this._setProperty(this.oData, FHIRUtils.deepClone(aResPath), oResourceServerState);
} else if (oRequestInfo.method === HTTPMethod.POST){
this._setProperty(this.oData, FHIRUtils.deepClone(aResPath));
this._setProperty(this.mResourceGroupId, FHIRUtils.deepClone(aResPath));
this._removeFromOrderResources(oBindingInfo);
} else if (oRequestInfo.method === HTTPMethod.DELETE){
this._removeFromRemovedResources(oBindingInfo);
}
this._setProperty(this.mChangedResources, FHIRUtils.deepClone(aResPath));
}.bind(this);
if (sResGroupId === sGroupId){
fnDoReset();
} else if (!sGroupId){
fnDoReset();
}
}.bind(this);
for ( var vType in this.mChangedResources) {
for (var vId in this.mChangedResources[vType]){
if (vType === "$_history"){
for (var vOriginId in this.mChangedResources[vType][vId]){
for (var sVersion in this.mChangedResources[vType][vId][vOriginId]){
oBindingInfo = this.getBindingInfo("/" + vType + "/" + vId + "/" + vOriginId + "/" + sVersion);
fnResetResource();
}
}
} else {
oBindingInfo = this.getBindingInfo("/" + vType + "/" + vId);
fnResetResource();
}
}
}
if (!bAvoidUpdate){
this.checkUpdate(true);
}
};
/**
* Removes a resource from the order resources map
*
* @param {sap.fhir.model.r4.lib.BindingInfo} oBindingInfo The binding info object
* @private
*/
FHIRModel.prototype._removeFromOrderResources = function(oBindingInfo){
var sType = oBindingInfo.getResourceType();
var sId = oBindingInfo.getResourceId();
var iIndex = this.mOrderResources[sType].indexOf(sType + "/" + sId);
this.mOrderResources[sType].splice(iIndex, 1);
if (this.mOrderResources[sType].length === 0){
delete this.mOrderResources[sType];
}
};
/**
* Removes a resource from the removed resources map
*
* @param {sap.fhir.model.r4.lib.BindingInfo} oBindingInfo The binding info object
* @private
*/
FHIRModel.prototype._removeFromRemovedResources = function (oBindingInfo) {
var sType = oBindingInfo.getResourceType();
var sId = oBindingInfo.getResourceId();
var iIndex = this.mRemovedResources[sType].indexOf(sType + "/" + sId);
this.mRemovedResources[sType].splice(iIndex, 1);
if (this.mRemovedResources[sType].length === 0) {
delete this.mRemovedResources[sType];
}
};
/**
* Cannot get a shared context for a path. Contexts are created by bindings instead and there
* may be multiple contexts for the same path.
*
* @throws {Error}
* @protected
* @see sap.ui.model.Model#getContext
* @since 1.0.0
* @override
*/
FHIRModel.prototype.getContext = function(){
throw new Error("Unsupported operation: sap.fhir.model.r4.FHIRModel#getContext");
};
/**
* Determines the URL of the StructureDefinition of a given resource instance
* Default URL would be base profile URL + resource type
*
* @param {object} oResource The FHIR resource
* @returns {string} The structure definition for the given binding info
* @protected
* @since 1.1.6
*/
FHIRModel.prototype.getStructureDefinitionUrl = function (oResource){
var sStrucDefUrl;
if (oResource && oResource.meta && oResource.meta.profile && oResource.meta.profile.length > 0) {
sStrucDefUrl = oResource.meta.profile[0];
} else if (oResource && oResource.resourceType) {
sStrucDefUrl = this.getBaseProfileUrl() + oResource.resourceType;
}
return sStrucDefUrl;
};
/**
* Determines whether server state variable needs an update
*
* @param {object} vServerValue Existing server state value
* @param {object} oResource The FHIR resource
* @param {sap.fhir.model.r4.HTTPMethod} sHTTPMethod HTTP Method for the resource
* @returns {boolean} if true then server state variable will not be updated
* @private
* @since 2.0.4
*/
FHIRModel.prototype._isServerStateUpToDate = function(vServerValue, oResource, sHTTPMethod){
if (!vServerValue && sHTTPMethod === HTTPMethod.PUT) {
return false;
} else if (vServerValue && sHTTPMethod === HTTPMethod.PUT && deepEqual(vServerValue, oResource)) {
// special handling when the server data and the client data before applying the change is the same (after multiple reset changes)
// forcefully update the existing server state
return false;
}
return true;
};
/**
* Determines the value of securesearch mode
*
* @returns {boolean} The value of secure search mode
* @protected
* @since 2.2.0
*/
FHIRModel.prototype.isSecureSearchModeEnabled = function () {
return this.bSecureSearch;
};
/**
* @typedef {object} sap.fhir.model.r4.NextLink
* @prop {string} url The url to which the request should fired
* @prop {sap.fhir.model.r4.FHIRListBinding.Parameter | sap.fhir.model.r4.FHIRTreeBinding.Parameter} mParameters The parameters that will be passed as query strings
* @public
* @since 2.3.2
*/
/**
* Determines the next link url should be used
*
* This method might be overridden by the application to provide a customized next link processing because FHIR did not offer a standardized link structure.
* @param {string} sNextLinkUrl The next link url
* @param {string} sPath The FHIR resource path
* @param {sap.fhir.model.r4.FHIRListBinding.Parameter | sap.fhir.model.r4.FHIRTreeBinding.Parameter} mParameters Existing parameters
* @returns {sap.fhir.model.r4.NextLink} Next link object containing the url and parameters
* @public
* @since 2.3.2
*/
FHIRModel.prototype.getNextLink = function (sNextLinkUrl, sPath, mParameters) {
var sQueryParams = sNextLinkUrl.substring(sNextLinkUrl.indexOf("?") + 1, sNextLinkUrl.length);
var aParameter = sQueryParams ? sQueryParams.split("&") : [];
var aKeyValue;
for (var i = 0; i < aParameter.length; i++) {
aKeyValue = aParameter[i].split("=");
mParameters.urlParameters[aKeyValue[0]] = aKeyValue[1];
}
return { url: sPath, parameters: mParameters };
};
return FHIRModel;
});