|
Blogs
BSP pages contain, effectively, HTML. When binary objects are requested, they are placed in the MIME repository and referenced from there. However, it is often necessary to handle binary objects/documents during the runtime of the program. It is not feasible to place these runtime documents in the MIME repository. (For any change in the MIME repository, transport records are written. This is usually not possible on a productive system, and is relatively slow compared to the runtime requirements of the running BSP application.) Typical (real!) examples that we have seen:
It would be interesting to look at three different ways to use/do this:
We've taken on a large challenge today, so let's get cracking! Test HarnessOur first step is to build a small test program to have a document available to display. As we don't feel like generating PDF documents on the fly or reading images from some database table, we'll just upload the test document. In all cases, we assume that either an image (.jpg, gif or .png) or some "known" document (.pdf, .doc, .xls, etc.) is specified. After the document is uploaded, we have it "in our hands" and must do something with it. Keep in mind that after the response is processed, our session will be closed (stateless program). First, we create a new BSP application and add a few page attributes: <!--code--> file_length TYPE STRING <!--code--> file_mime_type TYPE STRING <!--code--> file_name TYPE STRING <!--code--> file_content TYPE XSTRING <!--code--> display_type TYPE STRING <!--code--> display_url TYPE STRING The four file_* attributes reflect the dynamic document that we "created" via an upload. Note that the content is of type XSTRING because we are working with binary documents. The next step is to write the BSP application that will do the upload. After many hours: <!--code--> Layout: <!--code--> <!--code--> <%@page language="abap" %> <!--code--> <%@extension name="htmlb" prefix="htmlb" %> <!--code--> <!--code--> <htmlb:content design="design2002" > <!--code--> <htmlb:page> <!--code--> <htmlb:form method = "post" <!--code--> encodingType = "multipart/form-data" > <!--code--> <!--code--> <htmlb:radioButtonGroup id="display_type" > <!--code--> <htmlb:radioButton id = "inline" text="Display Inline" /> <!--code--> <htmlb:radioButton id = "html" text="Display Inside HTML Page" /> <!--code--> <htmlb:radioButton id = "window" text="Display In New Window" /> <!--code--> </htmlb:radioButtonGroup> <!--code--> <!--code--> <htmlb:fileUpload id = "myUpload" <!--code--> onUpload = "HandleUpload" <!--code--> upload_text = "Display" <!--code--> size = "90" /> <!--code--> <!--code--> <hr> <!--code--> <br>Name = <%= file_name%> <!--code--> <br>MIME-Type = <%= file_mime_type%> <!--code--> <br>Length = <%= file_length%> <!--code--> <!--code--> </htmlb:form> <!--code--> </htmlb:page> <!--code--> </htmlb:content> Here the <htmlb:*> BSP extension is used to create two controls on the Web page. The first is a radiobutton group to see which of the test cases to execute, the next is the file upload control. The encodingType attribute set for the <htmlb:form> is very important! This is absolutely required for file uploading. The last and most important aspect is to retrieve the uploaded document from the HTTP request and to fill our page attributes with the correct values. This is done in the onInputProcessing event handler. One more coffee, and we have: <!--code--> OnInputProcessing: <!--code--> <!--code--> DATA: fileUpload TYPE REF TO CL_HTMLB_FILEUPLOAD. <!--code--> fileUpload ?= CL_HTMLB_MANAGER=>GET_DATA( <!--code--> request = request <!--code--> id = 'myUpload' <!--code--> name = 'fileUpload' ). <!--code--> <!--code--> file_name = fileUpload->file_name. <!--code--> file_mime_type = fileUpload->file_content_type. <!--code--> file_length = fileUpload->file_length. <!--code--> file_content = fileUpload->file_content. <!--code--> <!--code--> DATA: radioButtonGroup TYPE REF TO CL_HTMLB_RADIOBUTTONGROUP. <!--code--> radioButtonGroup ?= CL_HTMLB_MANAGER=>GET_DATA( <!--code--> request = request <!--code--> id = 'display_type' <!--code--> name = 'radioButtonGroup' ). <!--code--> <!--code--> display_type = radioButtonGroup->selection. The final results in the browser are:
Display Document InlineWith this simplest approach, we have an incoming HTTP request for an HTML page. Instead of processing the BSP page (thus rendering out HTML code), we just send out the dynamic document that we uploaded, so that it is displayed inline. What do we have to do? Effectively, write the content that we already have available into the HTTP response and set the correct content data. Lastly, inform the BSP runtime that the response has been completely written, and that no further processing is required. <!--code--> OnInputProcessing: <!--code--> <!--code--> ... code previously displayed above ... <!--code--> <!--code--> IF display_type = 'inline' AND XSTRLEN( file_content ) > 0. <!--code--> DATA: response TYPE REF TO if_http_response. <!--code--> response = runtime->server->response. <!--code--> response->set_data( file_content ). <!--code--> response->set_header_field( name = if_http_header_fields=>content_type <!--code--> value = file_mime_type ). <!--code--> " response->set_header_field( name = if_http_header_fields=>content_length <!--code--> " value = file_length ). <!--code--> response->delete_header_field( name = if_http_header_fields=>cache_control ). <!--code--> response->delete_header_field( name = if_http_header_fields=>expires ). <!--code--> response->delete_header_field( name = if_http_header_fields=>pragma ). <!--code--> navigation->response_complete( ). " signal that response is complete <!--code--> RETURN. <!--code--> ENDIF. The IF statement checks that it is the inline test and that we actually have content available to display. The set_data() method writes the complete XSTRING into the response. The "Content-Type" HTTP header field is set. This MIME type is critical so the browser knows what is coming down the pipe. Theoretically we also want to set the "Content-Length" HTTP header field. However, we found a small bug in the kernel :(. If we set the content length, and the response is also gzip'd when it is streamed to the browser, the content length is not reset to the shorter length. Therefore the browser waits forever for more data that is not coming. So, we use a trick. We don't set the content length, and just let ICM set it after the gzip compression. (From our ICM expert: One additional remark I forgot to mention: it is good programming practice within our HTTP framework to never manually set the content length field. It is the kernel serialize code's task to determine the actual serialized length. This may depend on various side effects like compression, chunking, etc.) One can also consider deleting the three HTTP headers "Cache-Control", "Expires" and "Pragma". As BSP pages are effectively HTML pages with business data, the BSP runtime already pre-set these HTTP headers to indicate that BSP pages must not be cached. However, we are now reusing the HTTP response for our binary document. We can delete these headers. The last problem is that after the onInputProcessing method, the layout is also processed. This results in all output from the layout also being written into the response. Thus the response_complete() call, which informs the BSP runtime that the response is completed and no further processing is required. After the BSP application runs, we select a nice photo and see the picture displayed directly inside the browser window.
Display Document Inside HTML PageDisplaying the document inside the HTML page is slightly more complex. The problem is that in the response we must write HTML coding for the browser to render the page. The HTML coding must reference the dynamic document that we have available at this very moment. So where should we park the dynamic document until the browser has time to fetch it? (Keep in mind that once the response has been processed, the session will be closed and we will lose everything we had on hand.) The solution is actually very simple and elegant. The ICM supports an excellent HTTP cache. Whenever a MIME object is retrieved from Web AS, it's also added into the ICM cache. All other requests for the same document are served directly from the cache and do not require a switch to ABAP. These requests are processed in the kernel. When we have the dynamic document on hand, we can just as well write it into the ICM cache. Thus, any HTTP requests for the document (actually for this specific URL!) will retrieve the document from the cache. So the first interesting part of the code is to write the dynamic document directly into the ICM cache. The complete coding is: <!--code--> OnInputProcessing: <!--code--> <!--code--> ... code previously displayed above ... <!--code--> <!--code--> IF display_type = 'html' AND XSTRLEN( file_content ) > 0. <!--code--> <!--code--> DATA: cached_response TYPE REF TO if_http_response. <!--code--> CREATE OBJECT cached_response TYPE CL_HTTP_RESPONSE EXPORTING add_c_msg = 1. <!--code--> <!--code--> cached_response->set_data( file_content ). <!--code--> cached_response->set_header_field( name = if_http_header_fields=>content_type <!--code--> value = file_mime_type ). <!--code--> cached_response->set_status( code = 200 reason = 'OK' ). <!--code--> cached_response->server_cache_expire_rel( expires_rel = 180 ). <!--code--> <!--code--> DATA: guid TYPE guid_32. <!--code--> CALL FUNCTION 'GUID_CREATE' IMPORTING ev_guid_32 = guid. <!--code--> CONCATENATE runtime->application_url '/' guid INTO display_url. <!--code--> <!--code--> cl_http_server=>server_cache_upload( url = display_url <!--code--> response = cached_response ). <!--code--> RETURN. <!--code--> <!--code--> ENDIF. To write the information into the ICM cache, it's necessary to create a complete HTTP response. Keep in mind that the browser will later send an HTTP request for this document, to which the ICM cache will return the cached HTTP response directly. First, we create a new HTTP response object and add a new message. (The message is the actual buffers required to move the document from the ABAP VM into the kernel.) The next few lines were already discussed above. We set the content into the response and the content type. The set_status() call is required to indicate to the browser that for this request-response cycle everything went perfectly. The next aspect is to set the time that the dynamic document will stay in the ICM cache. Keep in mind that this time should be long enough for the browser to load all URLs referenced in the HTML page. However, there is no need to leave the document in the ICM cache for too long. Here a value of 3 minutes (180 seconds) is used. Anything between 1 and 5 minutes should be OK. The more difficult problem is the URL to use. This URL is effectively the "address" of the dynamic document on the server. The browser will later fetch the document from the server with this key. The first idea was to use the uploaded filename as part of the URL. In this case , take care to replace the ':' and '/' characters in the URL to make it a new valid URL. However, such a static type of URL does not scale very well. What happens if different people are running the same application, and uploading the same generic document (example: "travel_expenses.xls")? Then each new response will overwrite the previous copy in the cache. Therefore, the recommendation is to use some form of random number (GUID)in the URL that is generated. We could place the generated URL anywhere into the "namespace" of valid URLs. However, we recommend placing the URL into the "namespace" of the current active BSP application. Another nice touch you could consider is to also copy the document extension from the uploaded filename over into the URL. However, this is not critical. It's more important that the MIME type is set correctly in the HTTP response, which we already do. The last step is to place the document into the ICM cache. With the above coding, we successfully created a new HTTP response in the ICM cache that can be addressed under the URL stored in "display_url" (page attribute of type STRING). The last step is to change the rendered HTML coding to also display the uploaded document. For this we just use an <iframe>. The following HTML sequence is added in the layout, just before the end of the page. <!--code--> Layout: <!--code--> <!--code--> ... code previously displayed above ... <!--code--> <!--code--> <% IF display_type = 'html' AND display_url IS NOT INITIAL. %> <!--code--> <iframe src="<%=display_url%>" width="100%" height="500px"> <!--code--> </iframe> <!--code--> <% ENDIF. %> <!--code--> <!--code--> </htmlb:page> <!--code--> </htmlb:content> This is just an IF-guard to check for the specific case of displaying the document inline, plus the <iframe> sequence to load the newly created URL. The output is as expected. Both the data about the dynamic document and the document itself are displayed.
With this approach there are just two smaller problems that should not be forgotten. By default, the ICM cache is always configured. What happens if it is not available? You should really highlight this in the product documentation. The second problem is if the ICM cache is too small or flushed by someone. In that case we will get the dreaded Display Document In New WindowBy now most of the difficult work is complete. For the final leg of our explorations, we would like to place the dynamic document into a new window. The biggest problem is just how to trigger opening a new window. It is not possible to open a new browser window from the server. The simplest technique is to open the new window directly in the browser with a small JavaScript sequence: "window.open(url)". The first step is to require the dynamic document stored as a URL on the server. All this coding is already in place. We just change the IF-guard to include this new case. <!--code--> OnInputProcessing: <!--code--> <!--code--> ... code previously displayed above ... <!--code--> <!--code--> IF ( display_type = 'html' OR display_type = "window" ) AND <!--code--> XSTRLEN( file_content ) > 0. <!--code--> <!--code--> ... code as previously displayed ... <!--code--> <!--code--> ENDIF. Next, add the code in the layout to open the new window. This is quickly done with:
<!--code--> Layout:
<!--code-->
<!--code--> ... code previously displayed above ...
<!--code-->
<!--code--> <% IF display_type = 'window' AND display_url IS NOT INITIAL. %>
<!--code--> <script language="Javascript">
<!--code--> window.open("<%=display_url%>").focus();
<!--code--> </script>
<!--code--> <% ENDIF. %>
<!--code-->
<!--code--> </htmlb:page>
<!--code--> </htmlb:content>
With the final results:
Sidebar: STRING versus XSTRINGFor binary documents, using XSTRINGS is always recommended. However, there are cases where it is interesting to have the data available as STRING, even if only for debugging! Other examples are when the data is actual text readable, an example is XML data, and must be manipulated. One technique is to use the character interface of the request/response objects. For this, the get_cdata() and set_cdata() methods exist. The alternative is to convert the XSTRING into a STRING (only if possible!). The simple sequence "string = xstring" does not work! This can be done in ABAP using the following classes: CL_ABAP_CONV_IN_CE and CL_ABAP_CONV_OUT_CE. The documentation for these classes is seriously recommended. Example coding for converting an XSTRING to a STRING is: <!--code--> DATA: conv TYPE REF TO CL_ABAP_CONV_IN_CE. <!--code--> conv = CL_ABAP_CONV_IN_CE=>CREATE( input = Xcontent ). <!--code--> conv->READ( importing data = content len = len ). Sidebar: Handling (PDF) Data from TablesA long time ago we debugged for hours to understand why we couldn't display a PDF document correctly. The document was created by some function module, and if downloaded with SAP GUI worked perfectly. However, in HTTP context it was corrupted every time. The simple coding used was: <!--code--> CALL FUNCTION 'GIME_PDF' RECEIVING table_of_pdf_data. <!--code--> LOOP AT table_of_pdf_data INTO line. <!--code--> CONCATENATE content line INTO content. <!--code--> ENDLOOP. PDF data is text data that can be handled with normal STRING operations. However, it often happened that "line" contained trailing spaces, which were separator tokens in the PDF document. For ABAP, trailing spaces have no further meaning and they are completely ignored. So these trailing spaces never made it into the "content" string. Now the final string contained tokens that were not space separated anymore. Boom. Crash. Burn. The simple solution: use character mode when concatenating the strings together. <!--code--> CONCATENATE content line INTO content IN CHARACTER MODE. Sidebar: Content-DispositionOnce or twice there were requests to influence the way that the browser handles the binary document. Some want it always displayed. Others want it always saved. With the HTTP header "Content-Disposition" it is possible to indicate to the browser that the file must be displayed or must be saved. It is also possible to specify a file name. This header field is not discussed here in detail. Two examples are shown below. For the complete specification, please look at RFC 2183 (any good search engine will find it!). <!--code--> response->set_header_field( name = 'Content-Disposition' <!--code--> value = 'attachment; filename=myFile.xyz' ). <!--code--> response->set_header_field( name = 'Content-Disposition' <!--code--> value = 'inline' ). One for the RoadIf you like the picture, send me a short e-mail and you can have a copy. A colleague nearly drove over me for this shot! If you don't like it, just say nothing! Brian McKellar is a development architect for BSP and Web Dynpro ABAP. Duh, thought we were chasing a VP or something here. Better luck next time.
| |||||||||||||||||||||||