.xsodata services uing UI5sap.ui.define(). There is not even a MVC architecture, so no XML views or controllers; no Component configuration, and no application descriptor (manifest.json).index.html, which contains 2 <script> tags:<script
src="https://sapui5.hana.ondemand.com/1.71.5/resources/sap-ui-core.js"
id="sap-ui-bootstrap"
data-sap-ui-libs="
sap.ui.core,
sap.ui.layout,
sap.ui.unified,
sap.ui.table,
sap.ui.commons,
sap.m
"
data-sap-ui-theme="sap_bluecrystal"
>
</script>
<script src="index.js" type="text/javascript"></script>index.js, which contains the implementation.sap.ui.model.odata.v2.ODataModel, which we instantiated somewhere in the top of our index.js:var pkg = document.location.pathname.split('/').slice(1, -2);
var odataService = [].concat(pkg, ['service', 'ta']);
/**
* OData
*/
var modelName = 'data';
var model = new sap.ui.model.odata.v2.ODataModel('/' + odataService.join('/') + '.xsodata', {
disableHeadRequestForToken: true
});It's not necessary to go over the model instantiation in detail - for now it is enough to know that upon instantiation, the model is passed the uri of the .xsodata service we already built. We obtain the url in the code preceding the model instantiation by taking the url of the current webpage and building a path to service/ta.xsodata relative to that location:var pkg = document.location.pathname.split('/').slice(1, -2);
var odataService = [].concat(pkg, ['service', 'ta']); sap.ui.unified.FileUploader control. Here's the relevant code from index.js that instantiates the control:var fileUploader = new sap.ui.unified.FileUploader({
buttonText: 'Browse File...',
change: onFileToUploadChanged
busyIndicatorDelay: 0
});
sap.ui.unified.FileUploader control is presented to the user as a input field and a button to open a file chooser. This lets the user browse and pick a file from their client device.sap.ui.unified.FileUploader control provides events, configuration options and methods to validate the user's choice, and to send the file off to a server. For example, you can set the uploadUrl property to specify where to send the file to, and there's an upload() method to let the control do the request.sap.ui.unified.FileUploader control. To keep track of the user's choice, we configured a handler for the change event, which gets called whenever the user chooses a file, or cancels the choice.var fileToUpload;
var fileToUploadExists;
function onFileToUploadChanged(event){
fileToUpload = null;
fileToUploadExists = false;
var files = event.getParameter('files');
if (files.length === 0) {
initFileUploadDialog();
return;
}
fileToUpload = files[0];
...more code here...
}
fileToUpload variable to keep track of the user's choice. We need to keep track of it somewhere, since the choosing of the file and the upload are separate tasks with regards to the UI: choosing the file happens when the user hits the Browse button provided by the sap.ui.unified.FileUploader control, wheras the upload is triggered by hitting the confirm button of the upload dialog.sap.ui.unified.FileUploader will fire the change event, and our handler onFileToUploadChanged() gets called and passed the event as an argument. This event provides access to the FileList object associated with the file chooser: var files = event.getParameter('files');FileList is not part of UI5. Rather, it is one of a number of brower built-in objects, which together form the Web File API. We would have loved to obtain the FileList or the File object from our sap.ui.unified.FileUploader control directly by using a getter or something like that, but at the time we found no such method, and settled for a handler in the change event.FileList, we can check whether the user selected any files, and either disable the upload confirmation button (if no file was selected), or assign the chosen file to our fileToUpload variable so we can refer to it when the upload is confirmed: function onFileToUploadChanged(event){
...
if (files.length === 0) {
initFileUploadDialog();
return;
}
fileToUpload = files[0];
....
}If we pass the check, our variable fileToUpload will now contain the File object reflecting the user's choice. (Note that this object too is not a UI5 object, it's also part of the Web File API.)sap.ui.unified.FileUploader could have contained more than one file. But the default behavior is to let the user choose only one file. You can override that behavior by setting the sap.ui.unified.FileUploader's multiple property to true. Because we know that in this case, there can be at most only one file, we only need to check whether there is a file or not - there's no need to consider muliple files.function onFileToUploadChanged(event){
...
fileToUpload = files[0];
fileUploader.setBusy(true);
model.read('/' + filesEntityName, {
filters: [new sap.ui.model.Filter({
path: fileNamePath,
operator: sap.ui.model.FilterOperator.EQ,
value1: fileToUpload.name
})],
urlParameters: {
$select: [fileNamePath, 'FILE_LAST_MODIFIED']
},
success: function(data){
...update state depending upon whether the file exists...
},
error: function(error){
...update state to inform the user of an error...
}
});
....
}read() method which can be used to query the backend OData service. The first argument to the read() method is the so-called path, which identifies the OData EntitySet we want to query. In this case, we are interested in the Files EntitySet, as this corresonds to our CT_FILE database table in our backend. Because we use the name of the Files EntitySet in a lot of places, we stored it in the filesEntityName variable. So, our path becomes: '/' + filesEntityNameread() method takes a second argument, which is an object of query options. We'll highlight the few we need here.filters option: filters: [new sap.ui.model.Filter({
path: fileNamePath,
operator: sap.ui.model.FilterOperator.EQ,
value1: fileToUpload.name
})],
The filters option takes an array of sap.ui.model.Filter objects. When we instantiate the sap.ui.model.Filter object, we pass an object with the following configuration options:path - this should get a value that refers to a property defined by the OData entity type of this Entity Set. It corresponds to the name of a column of our database table. In this case, it is set to fileNamePath, which is a variable we initialized with 'FILE_NAME', i.e., the name of the column in the CT_FILE table that holds the name of our files.value1 - this should be the literal value that we want to use in our filter. In this case, we want to look for files with the same name as the file chosen by the user, so we set it to the name property of the File object that the user selected - fileToUpload.nameoperator - this should be one of the values defined by the sap.ui.model.FilterOperator object, which defines how the given filter value should be compared to the value of the column. In this case the operator is sap.ui.model.FilterOperator.EQ, which stands for an equals comparison. By using this operator, we demand that the value of the column should be exactly the same as the name of the chosen file. urlParameters: {
$select: [fileNamePath, 'FILE_LAST_MODIFIED']
},
This specifies for which columns we want to retrieve the values from the backend. It may be omitted, but in that case, all columns would be returned. Often this will not be a problem, but in this case, we really want to prevent the server from returning the values for the FILE_CONTENT column. Always retrieving the file contents would be an unnessary burden for both the front- and the backend so we actively suppress the default behavior. The only columns requested here are FILE_NAME and FILE_LAST_MODIFIED. The latter is currently unused but might come in handy to provide even more information to the user so they can better decide whether they want to re-upload an existing file.read() method have nothing todo with the request, but are callback functions for handling the result of the read request. The error callback gets called if there is some kind of issue with the request itself - maybe the backend has gone away, or maybe the structure of the service changed. The success callback is called when the read request executes normally, and any results are then passed to it as argument. This is even true if no results are found - the callback then simply gets passed an empty list of results.success callback is to flag whether the file already exists, and to update the state of the file uploader accordingly to inform the user. The existence of the file is flagged by assigning the fileToUploadExists variable, and we will see its significance in the next section where we discuss the implementation of the upload of the file contents.fileToUpload and fileAlreadyExists. This is all we need to handle the upload.press event, where we've attached the function uploadFile as handler.fileAlreadyExists variable and take the appopriate action:fileAlreadyExists is false, we should tell our model to add a new item. This is done by calling the createEntry()-methodfileAlreadyExists is true, we should tell our model to update the existing item. This is done by calling the update()-method'/' + filesEntityName(Note: this is exactly the same as the path we used in the read() call to figure out whether the file already exists.)sap.ui.model.odata.v2.ODataModel model provides the createKey() method which constructs such a path, including the key part, based on the values of the properties that make up the key. So, the code to construct the path for the update method becomes:'/' + model.createKey(filesEntityName, {"FILE_NAME": fileToUpload.name})(For more detailed information about how OData keys and paths work, see ODAta Uri Conventions, in particular the section on "Adressing Entries".)createEntry() and update() methods of the sap.ui.model.odata.v2.ODataModel in the past without any problems. It is normally quite intuitive and hasslefree: you simply specify an Object, and specify keys that match the property names of the target entity set, and assign JavaScript values, just as-is. So, if we disregard the FILE_CONTENT field for a moment, the payload for the Files entity set could be something like this:var payload = {
"FILE_NAME": fileToUpload.name,
"FILE_TYPE": fileToUpload.type,
"FILE_LAST_MODIFIED": new Date(fileToUpload.lastModified),
"FILE_SIZE": fileToUpload.size,
"FILE_LAST_UPLOADED": new Date(Date.now())
};Let's compare this to the data types of the corresponding properties in the entity type of the entity set:<EntityType Name="FilesType">
<Key>
<PropertyRef Name="FILE_NAME"/>
</Key>
<Property Name="FILE_NAME" Type="Edm.String" Nullable="false" MaxLength="256"/>
<Property Name="FILE_TYPE" Type="Edm.String" Nullable="false" MaxLength="256"/>
<Property Name="FILE_LAST_MODIFIED" Type="Edm.DateTime" Nullable="false"/>
<Property Name="FILE_SIZE" Type="Edm.Int32" Nullable="false"/>
<Property Name="FILE_CONTENT" Type="Edm.Binary" Nullable="false"/>
<Property Name="FILE_LAST_UPLOADED" Type="Edm.DateTime" Nullable="false"/>
</EntityType>
(Note: this entity type is taken from the $metadata document of our service.)Strings may be assigned to Edm.Strings, JavaScript Date objects may be assigned to Edm.DateTimes, and JavaScript Numbers may be assigned to Edm.Int32s.Strings, Numbers, Booleans (and arrays and objects containing values of those types). The native JSON type system is simply too minimal to represent all the types in the Edm Type system used by OData, hence the need for an extra JSON representation format. The sap.ui.model.odata.v2.ODataModel does a pretty remarkable job in hiding all this complexity and making sure things work relatively painless.FILE_CONTENT in the payloadFILE_CONTENT property. In the entity type, we notice that the data type is Edm.Binary. What would be the proper JavaScript runtime type to construct the payload?sap.ui.model.odata.v2.ODataModel. So we might be tempted to simply pass the File object itself directly as value for the FILE_CONTENT property. But when we call either the createEntry or update method with a payload like this:var payload = {
"FILE_NAME": fileToUpload.name,
"FILE_TYPE": fileToUpload.type,
"FILE_LAST_MODIFIED": new Date(fileToUpload.lastModified),
"FILE_SIZE": fileToUpload.size,
"FILE_LAST_UPLOADED": new Date(Date.now()),
"FILE_CONTENT": fileToUpload
}; we get an error in the response:The serialized resource has an invalid value in member 'FILE_CONTENT'.So clearly, the sap.ui.model.odata.v2.ODataModel needs some help here.File object being a little bit too specific for UI5 - after all, a File object is not just some binary value, but it is a sublclass of the Blob object, which has all kinds of file-specific properties of itself. However, assigning a proper, plain Blob object in the payload yields exactly the same result, so that's not it either.Edm.Binary type. In the part about the JSON representation (See: "4. Primitive Types") we found this: Base64 encoded value of an EDM.Binary value represented as a JSON stringIt seems to suggest the whatever thing that represents the Edm.Binary value need to be Base64 encoded, which yields a string value at runtime, and this string may then be serialized to a JSON string. So, if we could make a Base64 encoded string value of our binary value, we could assign that in the payload. (We already saw that sap.ui.model.odata.v2.ODataModel will turn JavaScript String values to a JSON representation so we don't have to do that step ourselves.)btoa() does this for us.Edm.Binary value is. We found something in the section about Primitive Datatypes on how to create literal Edm.Binary Values:binary'[A-Fa-f0-9][A-Fa-f0-9]*' OR X '[A-Fa-f0-9][A-Fa-f0-9]*'
NOTE: X and binary are case sensitive.
Spaces are not allowed between binary and the quoted portion.
Spaces are not allowed between X and the quoted portion.
Odd pairs of hex digits are not allowed.
Example 1: X'23AB'
Example 2: binary'23ABFF'X or binary to it. At runtime, this would then be a JavaScript string value reprenting an Edm.Binary literal, which we could then turn into its Base64 encoded value, and send assign to the payload.Edm.Binary literal representation of the document but otherwise useless. At this point the solution was clear though - just leave out the intermediate step of converting the original value to an Edm.Binary literal.uploadFile functionFile object stored in the fileToUpload variable, and a flag fileToUploadExists is set to true or false depending upon whether the file is already stored in the backend table. This is code we ended up with for uploading the file:function uploadFile(){
var fileReader = new FileReader();
fileReader.onload = function(event){
var binaryString = event.target.result;
var payload = {
"FILE_NAME": fileToUpload.name,
"FILE_TYPE": fileToUpload.type,
"FILE_LAST_MODIFIED": new Date(fileToUpload.lastModified),
"FILE_SIZE": fileToUpload.size,
"FILE_CONTENT": btoa(binaryString),
"FILE_LAST_UPLOADED": new Date(Date.now())
};
if (fileToUploadExists) {
model.update(
'/' + model.createKey(filesEntityName, {
"FILE_NAME": fileToUpload.name
}),
payload
);
}
else {
model.createEntry('/' + filesEntityName, {
properties: payload
});
}
model.submitChanges({
success: function(){
closeUploadDialog();
}
});
};
fileReader.readAsBinaryString(fileToUpload);
}As explained earlier, uploading the file breaks down into 2 subtasks, and this handler takes care of both:FileReader to read the contents of the File objectcreateEntry or the update method based on whether the file already exists, passing the path and the payload.FileReader to read the contents of a File objectFileReader, which is also part of the Web File API. To get the contents of a File object, we can call one of the FileReader's read methods.FileReader'sread methods do not return the contents of the file directly: the Web File API is mostly asynchronous. Instead, we have to attach an event handler to the FileReader which can respond to the FileReader's events. In this case we overrided the FileReader's onload() method, which gets called when the FileReader is done reading a File. (Instead of the override, we could also have attached a handler with addEventListener but it really doesn't matter too much how the handler is attached.)read() method and wait for the reader to call our onload() handler.function uploadFile(){
var fileReader = new FileReader();
fileReader.onload = function(event){
var binaryString = event.target.result;
...do something with the file contents...
};
fileReader.readAsBinaryString(fileToUpload);
}FileReader provides a number of different read methods, and the chosen method determines the type of the value that will be available in event.target.result by the time the load handler is called. Today, the FileReader provides:To figure out which method we should use, we should consider how our backend expects to receive the data. Or rather, how our sap.ui.model.odata.v2.ODataModel wants us to pass the data so it can do the appropriate call to the backend. In a previous section we already explained our struggle to figure out how to represent a Edm.Binary value in the payload, and based on those findings, readAsBinaryString() is the appropriate method. With this read method, the FileReader turns each individual byte of the file contents in to a JavaScript character, much like the fromCharCode()-method of the String object would do. The resulting value is a JavasScript binary string: each character represents a byte.readAsText() method would do: that would attempt to read the bytes as if they are encoded characters in UTF-8 encoding, in other words it would result in a character string, not a binary string.sap.ui.unified.FileUploader to present a file chooser to the usersap.ui.unified.FileUploader's change event to get a hold of the File object representing the user's selection.FileReader to read the contents of a File objectsap.ui.model.odata.v2.ODataModel's read() method to specify a query using a sap.ui.model.Filter to check whether an item already exists in the backend.createEntry() and update() methods of the sap.ui.model.odata.v2.ODataModel to create or update an entry in the backend.btoa() function to create payloads for OData properties of the Edm.Binary typesap.ui.model.odata.v2.ODataModel as well as the SAP HANA .xsodata service that backs it, we still haven't found any official documentation, either from the OData specification or from SAP that confirms that this is really the correct way. We would hope that SAP HANA's .xsodata implementation is a faithful implementation of the standard, but for the Edm.Binary type, I'm just not 100% sure. If anybody could chime in and confirm this, and preferably point me to something in the OData specification that confirms this, then I would be most grateful.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 36 | |
| 31 | |
| 31 | |
| 28 | |
| 26 | |
| 26 | |
| 20 | |
| 15 | |
| 12 | |
| 11 |