Ressourcenintensive Operationen im Web

Damit eine Webanwendung als flüssig bedienbar empfunden wird, müssen die Antwortzeiten kurz sein. Die serverseitige Bearbeitungszeit für einen Request sollte unter einer halben Sekunde liegen. Das BSP-Framework selbst ist mit Bearbeitungszeiten von unter 100 Millisekunden in dieser Hinsicht unproblematisch. Aber die im Model programmierte Geschäftslogik (komplexe Selektionen, Speichern komplexer Geschäftsobjekte) erfordert manchmal deutlich höhere Antwortzeiten. Was tun?

Performance-Optimierung

Eine Performanceanalyse der Anwendung ist sicher das erste, was man tun sollte. In den meisten Fällen sind nämlich ineffiziente ABAP-Codestrecken oder ungünstige Datenbankoperationen der Grund für die langen Laufzeiten. Warum aufwendige Workarounds wie Fortschrittsanzeigen austüfteln, wenn die Geschäftsfunktion, um die es geht, "tiefergelegt" werden kann. Einen Anfang können Sie mit den Standard-Analysetools des SAP-Systems machen, dem SQL-Trace (Transaktion st05) und der Laufzeitanalyse (Transaktion SE30), wie ich im Artikel über BSP-Profiling näher beschrieben habe. Diese Analysen sind ein guter Ausgangspunkt für Optimierungen.

Wenn die Probleme in kundeneigenem ABAP-Code (und nicht im SAP-Standardcode) liegen, können Sie auch Code-Optimierungen meist ohne grössere Probleme durchführen. Aber auch die Laufzeiten von SAP-Standardprogrammen lassen sich manchmal ohne Modifikationen verbessern, zum Beispiel durch Einführung eines geeigneten Datenbank-Index.

Die wichtigste Regel beim Optimieren ist, dass man sich nicht mit Mini-Optimierungen verzettelt, die für die Gesamtlaufzeit fast gar keine Rolle spielen. Ein guter Ausgangspunkt zum Optimieren ist beispielsweise dann gegeben, wenn die Hitlisten mit grossem Abstand von einer bestimmten Codestrecke (in se30) oder Datenbankoperation (in st05) angeführt werden. Wenn sich dagegen die Last relativ gleichmässig auf viele Einheiten verteilt, wird es schwieriger. Man kann dann schauen, ob der Code vielleicht insgesamt zu häufig ausgeführt wird. Beim Buchen komplexer Belege (Aufträge, Warenbewegungen o.ä.) kann man nicht benötigte (Teil-)Funktionen durch Customizing abschalten, so dass die zuständigen Code-Teile gar nicht mehr aufgerufen werden. Auch kann das Problem im Aufruf des API liegen: Vielleicht werden bei einer Selektion mehr Daten angefordert als tatsächlich benötigt werden? In manchen Schnittstellen kann man mittels einer Flagleiste genau spezifizieren, welche Daten man tatsächlich benötigt.

Wenn die Analysen aber zeigen, dass es einen klaren "Hauptschuldigen" für die langen Laufzeiten gibt, gibt es je nach Problem verschiedene Lösungsmöglichkeiten, um die Antwortzeiten herunterzubekommen. Einige typische Fälle sind:

Aufbereitung von Daten in periodischen Jobs

Wenn man sich im Rahmen einer Performanceanalyse anschaut, welche Benutzeranfragen das System besonders belasten, so fällt oft auf, dass es hunderte von Anfragen gibt, die auf Datenbankebene zu den selben Selektionen führen. Ein Beispiel aus der Praxis: In einem Retailsystem wird oft der Funktionsbaustein MERCHANDISE_GROUP_HIER_SELECT zum Lesen hierarchischer Warengruppeninformationen verwendet. Die Benutzer in den Filialen wollen die Warengruppen in einer Baumstruktur angezeigt bekommen, die sie gut kennen, und in der sie wissen, wie sie zu einem bestimmten Verkaufssektor navigieren können. Sie tun dies, um zum Beispiel zusätzliche Artikel zu bestellen, die über den automatischen Nachschub hinausgehen (Filialauftrag), oder auch nur, um Informationen zu den Artikeln in strukturierter Form zu lesen. Zu gewissen Stosszeiten führt dies dazu, dass hunderte von Prozessen die vielen Datenbanktabellen des Klassensystems (KLAH und Konsorten) und des Artikelstamms selektieren, um diese Struktur aufzubauen, obwohl die Struktur im Ergebnis für alle Benutzer fast identisch ist.

Solche Situationen rufen nach Pufferung! Man muss herausfinden, wie häufig sich die selektierten Daten tatsächlich ändern und kann die Daten in den Puffern der Applikationsrechner oder in INDX-artigen Datenbanktabellen vorhalten. In diesem Fall waren die Benutzer mit einer taggenauen Warengruppenhierarchie mehr als zufriefen. Es wurde daher ein nächtlicher Job Z_BUILD_WGHIER eingeplant, der diese Warengruppenhierarchie aufbaut und mittels export to database zusammen mit einem Verfallsdatum in einer Datenbanktabelle fortschreibt. Hier der Code des Funktionsbausteins zum gepufferten Lesen der Warengruppenhierarchie (mit einigen zusätzlichen Kommentaren aufbereitet, damit Sie dieses Codefragment besser nachvollziehen können):

* Prüfung auf gepufferten Datenbestand auf dem App-Server
* Den Key für die gewünschte Warengruppenhierarchie aufbauen (9 Level * 3 Sprachen)
    concatenate 'wghier_' out_level '_' sort_ber '_' lv_spras into lv_id.
    try.
* Zentrale Klasse für auf DB gepuffertes Lesen und Schreiben von Anwendungsdaten
* Löst Ausnahme ZCX_NOT_FOUND aus, wenn Daten im Puffer nicht gefunden werden
        zcl_application_buffer=>import( exporting iv_id     = lv_id
                                                  iv_use_db = 'X'
                                        importing ev_value  = lv_data ).
* Generischen Datencontainer lv_data (ein xstring) in konkrete Anwendungsdaten übersetzen
        import bossh_flat to gt_bossh
               cretim to lv_cretim
               from data buffer lv_data.

* Prüfen, ob die importierten Daten bereits veraltet sind
        get time stamp field lv_now.
        lv_diff = cl_abap_tstmp=>subtract( tstmp1 = lv_now
                                           tstmp2 = lv_cretim ).
* Das maximal erlaubte Alter der Daten ist durch einen Anwendungsparameter steuerbar
        if lv_diff > zcl_param=>get_single_value( 'zperformance_settings.lifespan_boss' ).
          raise exception type zcx_not_found.
        endif.

* Daten aus dem Puffer im Exportparameter bossh_flat zurückgeben
        bossh_flat[] = gt_bossh.

      catch zcx_not_found.
Ab hier die Warengruppenhierarchie auslesen
        call function 'MERCHANDISE_GROUP_HIER_SELECT'
          exporting
            spras     = lv_spras
            wg_bez    = 'X'
            wg_stufen = out_level
          tables
            o_wghier  = lt_wghier[]
          exceptions
            others    = 1.
        if sy-subrc <> 0.
          _message_raising hier_read_failure.
        endif.

* Rückgabetabelle für die Anwendung aufbereiten (ergibt bossh_flat[])
        [...]

* Zeit merken, zu der die Hierarchie erzeugt wurde
      get time stamp field lv_cretim.

* Daten auf dem Application Server vorhalten
      export bossh_flat from bossh_flat[]
             cretim from lv_cretim
             to data buffer lv_data.
      zcl_application_buffer=>export( exporting iv_id     = lv_id
                                                iv_use_db = 'X'
                                                iv_value  = lv_data ).

  endtry.
Der täglich eingeplante Job Z_GET_WGHIER ruft lediglich in allen 27 möglichen Kombinationen diesen Funktionsbaustein auf. So sind alle möglichen Aufrufe des Bausteins einmal ausgeführt, und die Ergebnisse wurden in einer kundeneigenen INDX-artigen Datenbanktabelle zwischengespeichert. Nachfolgende Aufrufe des Bausteins greifen auf diesen Puffer zu. Die Ersparnis ist enorm. Denn die INDX-artige Tabelle enthält die Daten bereits in der Form, wie sie in der Anwendung benötigt werden, während sie ungepuffert mühsam durch eine Folge von vielen verschiedenen Selektionen auf Tabellen des Klassensystems und des Artikelstamms konstruiert werden müssen.

Das Fortschreiben und Lesen der Daten wurde, wie Sie sehen, in die Klasse zcl_application_buffer delegiert, so dass es in vielen Kontexten nutzbar ist. Dass diese Delegation überhaupt nur möglich ist, haben wir technisch dem Feature zu verdanken, dass beliebige ABAP-Daten mit dem Konstrukt export ... to data buffer lv_xstring in einen xstring verpackt werden können. So kann die Schnittstelle der import()- und export()-Methoden agnostisch für die konkrete Gestalt der Anwendungsdaten entworfen werden. Die Klasse zcl_application_buffer ist übrigens Teil des BSP-MVC-Frameworks, der auf http://bsp.mits.ch/download zum Download bereitsteht.

Der wesentliche Performancegewinn ist durch diese Umstellung bereits erzielt. Was zuvor in hunderten von Sitzungen identisch ausgeführt wurde, wird nun für jede Aufgabe genau einmal am Tag selektiert. Eine weitere Optimierung ergibt sich noch durch die in der Klasse zcl_application_buffer eingebaute Zwischenspeicherung in den Puffern der Applikationsserver. Die Implementierung der Methode import() zeigt das Prinzip:

method export .

  data: lv_use_db        type flag,
        lv_id            type z_buffer_id,
        lv_len           type i,
        lv_len_act       type i.

*---------------------------------------------------------------------
* ZAPBUF-Satz zur ID gepuffert einlesen
*---------------------------------------------------------------------

* ID-Feld übernehmen
* Macro move_id validiert die übergebene ID (Länge < 55)
  move_id lv_id iv_id.

  lv_use_db = iv_use_db.

* Datum im Puffer dieses Servers fortschreiben
  try.
      export data from iv_value
             to shared buffer zapbuf(zb)
            id lv_id.
    catch cx_sy_export_buffer_no_memory.
* Ausnahme kann nur in seltenen Fällen auftreten (wenn
* die zu exportierenden Daten mehr Platz benötigen als
* im Shared Buffer noch zur Verfügung steht)
  endtry.

* Falls gewünscht, auch in DB fortschreiben
  if lv_use_db = 'X'.
    export data from iv_value
           to database zapbuf(zb)
           id lv_id.
  endif.

endmethod.
Ein notwendiger Hinweis bezüglich der Pufferung auf dem Applikationsrechner: Beachten Sie, dass das Produktivsystem in der Regel mit mehreren Applikationsrechnern konfiguriert ist. Wenn also Daten mit export to shared buffer auf dem Applikationsrechner zwischengespeichert werden, können diese Daten in anderen Sitzungen nicht gelesen werden.

Ausführung im Hintergrund

Manchmal lassen sich die Antwortzeiten mit keiner der aufgeführten Techniken optimieren – zum Beispiel wenn das Optimierungspotential in einer SAP-Standardtransaktion liegt. Wenn das grundlegende Daten- oder Programmiermodell betroffen ist, käme dies einer Neuprogrammierung des Standards gleich. Unter enormen Test- und Programmieraufwänden würden Teile der Anwendung vom Standard abgekoppelt – ein Preis, der in der Regel für eine reine Performanceoptimierung zu hoch ist. Auch eine vorgeschaltete Pufferung ist oft nicht möglich, weil z.B. Daten in Echtzeit selektiert werden müssen.

In solchen Fällen empfiehlt sich eine zweistufige Ausführung: Schreiben Sie die Parameter und die Art der auszuführenden Aktionen in einer Datenbanktabelle fort, die als Warteschlange für die langsame SAP-Standardtransaktion fungiert. Die Abarbeitung dieser Warteschlange erfolgt dann asynchron in einem zweiten Schritt. Der Benutzer erhält sofort eine Antwort, z.B. "Die angeforderte Warenbewegung wird in Kürze gebucht" und kann die Anwendung flüssig weiterverwenden.

Natürlich muss man dann eine zweite Anwendung vorsehen, in der der Benutzer über den Status seiner Hintergrundoperationen informiert wird. Und damit Sie nicht nur die Anwendung, die aktuell ein Problem macht, auf diese Weise über eine Warteschlange bedienen können, sondern den gleichen Mechanismus in Zukunft auch für andere Anwendungen zur Verfügung haben, ist man gut beraten, die verzögerte Ausführbarkeit als Feature aus dem aktuellen Anwendungscode herauszuabstrahieren und in Form eines Frameworks zur Verfügung zu stellen. Hier eine produktive Realisierung einer solchen allgemeinen Anwendung im Retail Store der Migros - der Hintergrundmonitor:

Aufgaben eines Hintergrundframeworks

Ein Framework für Hintergrundaufgaben muss Tasks auf Wunsch des Benutzers starten können. In einem Monitor muss der Benutzer jederzeit prüfen können, welche Tasks in welchem Status sind: Sind sie eingeplant, werden sie bereits ausgeführt, oder sind sie erledigt. Für die eingeplanten Aufgaben müssen auch die Eingangsparameter (etwa die vom Benutzer ausgewählten Selektionsoptionen) zwischengespeichert werden, damit sie zum Ausführungszeitpunkt verfügbar sind. Diese Funktionalität wird uns im wesentlichen bereits durch die Basisfunktionalität des tRFC abgenommen. Für erledigte Tasks müssen aber auch Ausgaben vorgehalten werden. Das können ABAP-Listen sein, wie sie durch Reportausgaben erzeugt werden, es kann aber auch HTML-Code, reiner Text oder ein pdf-Dokument sein. Die Verwaltung dieser Ausgaben wird uns von der Basis nicht geliefert, sondern muss dazuprogrammiert werden.

Aufgaben, die bei Ausführung einen Fehler ergaben, müssen durch einen besonderen Status als fehlerhaft gekennzeichnet sein. In diesem Fall muss man ein Application Log aufrufen können, das die gesammelten Fehlermeldungen aus der Anwendung enthält. Wenn kein Nachrichtensammler verwendet wird, muss zumindest eine Meldung ersichtlich sein, die den Abbruch erklärt. Der Benutzer sollte, wenn die Anwendungslogik das zulässt, die Möglichkeit haben, eine fehlgeschlagene Aufgabe wieder anzustarten, d.h. erneut auszuführen.

Manche Aufgaben sind nicht mehrfach anstartbar. Der Grund kann beispielsweise die Schonung von Ressourcen sein: Wenn ein Benutzer bereits eine bestimmte aufwendige Listselektion aufgerufen hat, soll er dies nicht ein zweites Mal tun dürfen. Es gibt ja auch keinen Grund dafür – die zweite Ausführung derselben Selektion belastet nur das System, führt aber auf dasselbe Ergebnis wie die erste. Ein Task muss also vor Ausführung befragt werden, ob er Einwände gegen sein Gestartetwerden hat.

Der transaktionale RFC (tRFC)

Für die technische Umsetzung kann man sehr schön auf dem sogenannten transaktionalen RFC (tRFC) von SAP aufsetzen. Dieser erlaubt die asynchrone Abarbeitung von Funktionsbausteinen, wenn sie mit dem Zusatz in background task aufgerufen werden. Für die Ausführung nötige Daten wie der Name des Funktionsbausteins, die Aktualparameter der Schnittstelle usw. werden bei einem solchen Aufruf vom Kernel in den Tabellen arfcsstate und arfcsdata fortgeschrieben, dann wird die Bearbeitung des ABAP-Codes unmittelbar nach dem Aufruf fortgesetzt. Die Abarbeitung selbst erfolgt dann asynchron; die Warteschlange kann von einem Administrator in der Transaktion sm58 überwacht werden.

Damit liegen die Grundlagen bereits in der ABAP-Basis vor, auf denen ein Hintergrundmonitor aufsetzen muss: Tasks werden registriert, bekommen eine Task-ID und haben in Tabelle arfcsstate einen Ausführungsstatus, der zeigt, ob der Task noch in der Queue steht, ob er bereits gestartet wurde, ob er korrekt beendet wurde oder ob er auf einen Fehler lief. Auch das tatsächliche Ausführen der Tasks mit allem, was dazu nötig ist, wird von der Basis automatisch erledigt. Ein Hintergrundmonitor muss nur noch die benötigte Zusatzfunktionalität bereitstellen.

Ein Hinweis noch zur Transaktions-ID: Die vom System vergebene Transaktions-ID ist ein aus 12 Bytes bestehendes Binärfeld, das die LUW eindeutig kennzeichnet, in deren Rahmen der tRFC abgesetzt wurde. Lassen Sie sich nicht davon beirren, dass der für diesen Schlüssel vorgesehene Datentyp ARFCTID scheinbar eine logische Struktur hat. Er ist von der Länge 24 und laut DDIC angeblich aus einer achtstelligen "IP-ID", einer vierstelligen Prozess-ID, einem achtstelligen Zeitstempel und einer vierstellige Transaktions-Kennung zusammengesetzt. Dies ist vermutlich ein Relikt aus einer früheren Implementierung des tRFC. In praxi werden die 24 Zeichen benutzt, um eine aus zwölf Bytes bestehende Transaktions-ID hexadezimal zu notieren. Es gibt auch den unstrukturierten Datentyp QRFCTID, der einfach nur aus 24 Zeichen besteht.

Werden innerhalb einer LUW (das heisst ohne zwischenzeitlichen COMMIT WORK) mehrere tRFC abgesetzt, so bekommen diese alle dieselbe Transaktions-ID. Es gibt daher in der Tabelle arfcsstate nach der Transaktions-ID noch ein weiteres Schlüsselfeld, einen Zähler, mit dem die tRFC-Aufrufe einer LUW durchnumeriert werden.

Um die Transaktions-ID, unter der Ihr tRFC abgelegt wurde, zu ermitteln, können Sie den Funktionsbaustein ID_OF_BACKGROUNDTASK verwenden:

* TID ist nach Absetzen des tRFC bekannt
  call function 'ID_OF_BACKGROUNDTASK'
    importing
      tid = ls_tid.

Wenn Sie sich bereits in der Abarbeitung eines tRFC befinden, können Sie die Transaktions-ID wie folgt beschaffen:

* ID für Datenablage besorgen
  call function 'REMOTEID_OF_BACKGROUNDTASK'
    importing
      tid = ls_tid.

Der Task

Ein Framework für Hintergrundverarbeitungen sollte es ermöglichen, dass verschiedenartige Tasks eingeplant werden können. In einem objektorientierten Modell liegt es nahe, jeden konkreten Task als Subtyp eines abstrakten Tasks zu implementieren. Im allgemeinen entspricht jeder Tasktyp einer bestimmten applikatorischen Aufgabe. Sie kommt mit einem Anwendungskey daher. Lautet die Aufgabe beispielsweise "Aufteiler xyz mit den Werten ... zurückmelden", so ist der Anwendungskey die Nummer des betreffenden Aufteilers. Die Werte gehen über die tRFC-Basisfunktionalität in die Tabelle ARFCSDATA, aus der sie beim konkreten Aufruf hervorgeholt werden.

Die öffentliche Schnittstelle, die jeder Task aus unserem Framework bedienen sollte, bestand aus den folgenden Aufgaben - ausser set_filiale() sind die meisten dieser Methoden vermutlich in jedem Hintergrundframework sinnvoll:

Der Task Scheduler

Die eigentliche Einplanung des Tasks geschieht mit dem Task Scheduler. Hier ein typischer Aufruf (für die Inventurbeleganlage), wiederum ergänzt mit Kommentaren für die bessere Nachvollziehbarkeit:
method process_crea_trfc .

  data: lo_ex         type ref to zcx_error,
        lo_scheduler  type ref to zif_trfc_scheduler.

  try.
* Instanz des Schedulers mit Objektfabrik beschaffen
* Dabei den konkreten Tasktyp gleich mitgeben
      lo_scheduler ?= zcl_factory=>get_object( iv_product = 'trfcScheduler'
                                               iv_id      = ztrfc_task_type-inv_crea ).
* Parameterübergabe an den Task
      lo_scheduler->set_parameter( iv_name  = 'gt_items'
                                   iv_value = it_items ).
      lo_scheduler->set_parameter( iv_name  = 'gs_control'
                                   iv_value = is_control ).
      lo_scheduler->set_filiale( filiale ).
* Task ausführen
      lo_scheduler->submit( ).

      ...

    catch zcx_error into lo_ex.
      message->add_from_exception( iv_condition = 'wsti'
                                   iv_exception = lo_ex ).
  endtry.


endmethod.
Die Methode submit() des Schedulers hat hierbei zentrale Bedeutung. Sie ruft nicht nur via Delegation das do() des konkreten Tasks auf, sondern versorgt auch die Statustabelle ztrfc_status mit den applikatorischen Zusatzinformationen (Tasktyp, Anwendungskey, Filiale, auch Erfasser und Zeitstempel der Erfassung):
method zif_trfc_scheduler~submit.

  data: ls_status type ztrfc_status,
        lo_ex     type ref to zcx_error.

* Eintrag in ZTRFC_STATUS vorbereiten
* Etwaige Exception ZCX_PARAMETER_ERROR auslösen
  fill_status( importing es_status = ls_status ).

  try.

* Hat der Task Einwände gegen sein Gestartetwerden?
      go_task->check_runnable( ).

* Task im Hintergrund absetzen
      go_task->do( ).

* Eintrag in ZTRFC_STATUS fortschreiben
      add_status( changing cs_status = ls_status ).

* Ist Task wiederanstartbar? Dann Parameter des Tasks speichern
      ls_status-status = ztrfc_execution_state-planned. " Temporär auf geplant stellen
      if go_task->is_restartable( ls_status ) ne space.
        save_parameters( exporting is_status = ls_status ).
      endif.
* Sicherheitshalber wiederherstellen
* (wird wichtig, falls hier später noch Code hinzugefügt werden sollte)
      ls_status-status = ztrfc_execution_state-running.

* Daten- und TRFC-Bearbeitung anstossen
      commit work.

    catch zcx_error into lo_ex.
      rollback work.
      raise exception type zcx_not_runnable
        exporting
          text = lo_ex->text.
  endtry.

endmethod.

Ergebnisse fortschreiben

Wenn der Funktionsbaustein im Background gestartet wird, weiss er nichts mehr vom Kontext des Aufrufers - mit Ausnahme der in seinen Importparametern übergebenen Werte. Der Prozess, in dem der Background-Call ausgeführt wurde, dürfte sowieso längst beendet sein. Der Background Task kann seine Ergebnisse also nur in Datenbanktabellen fortschreiben, die dem Benutzer über den Hintergrundmonitor zugänglich gemacht werden. Das Hintergrundframework der Migros kennt folgende Typen von Ergebnissen, die in verschiedenen Partitionen einer dafür vorgesehenen INDX-artigen Datenbanktabelle ztrfc_data fortgeschrieben werden: Der Funktionsbaustein, der dies leistet, heisst Z_TRFC_WRITE_DATA. Er sollte auf jeden Fall am Ende der tRFC-Funktion aufgerufen werden, um den Ausführungsstatus in Tabelle ztrfc_status abzuspeichern.

Die BSP-Applikation Hintergrundmonitor

Der Hintergrundmonitor selbst, den auch der obige Screenshot zeigt, ist nun nichts anderes als eine Sicht auf die beiden Tabellen ztrfc_status und ztrfc_data. In einer Übersicht werden die für den Benutzer relevanten Felder aus Tabelle ztrfc_status dargestellt. Wenn ein Application Log oder eine Jobausgabe vorliegt, gibt es ein verlinktes Icon, das in ein Detailbild führt. Dort wird die gewünschte Information dann dargestellt.

Die Liste benötigt gewisse für jeden Tasktyp gleiche Informationen zu den Tasks. Um diese Informationen bereitzustellen, gibt es eine Sammlung von Task-Deskriptoren - für jeden Tasktyp gibt es genau einen Descriptor. Diese Deskriptoren werden während der Aufbereitung der Liste im Sinne des Fliegengewichts-Entwurfsmusters mit konkreten taskspezifischen Informationen angereichert. Das Interface des Deskriptors zeigt die im Monitor benötigten Operationen:

Im Detailbild des Monitors wird schliesslich das pdf-Dokument oder ein Anwendungslog angezeigt. Der folgende Screenshot zeigt das Ergebnis einer Inventurauswertung mit der erschreckenden Gesamtzahl von 452 Seiten:

Die Grösse von 452 Seiten ist leider kein Versehen, sondern wurde so vom Geschäft gefordert. Offenbar werden für die Vorbereitung und Durchführung von Inventuren in grösseren Märkten wirklich Listen dieser Grösse benötigt (jedenfalls solange physisch mit Streichlisten inventiert wird und nicht mit MDE-Geräten). Wir hatten zuerst überlegt, die Ergebnisse der Selektion in Form von ABAP-Daten abzulegen und das pdf dann erst auf Anforderung im Hintergrundmonitor zu generieren. Aber allein die Konvertierung des zugrundeliegenden SAP-Formulars in PDF dauerte inakzeptabel lange. So entsteht als Ergebnis dieses Tasks nun direkt das pdf-Dokument, das als xstring, also als binäres Rohdatum, sehr performant mit der Anweisung import from database gelesen werden kann und in dieser Form direkt vom Client verarbeitet wird. Die eingebettete Sicht des Acrobat Readers wie im obigen Beispiel entsteht übrigens im Browser automatisch, weil das pdf-Dokument in einem iframe steht, dessen Content-Type application/pdf ist.

Ein paar Worte noch zur Aufbereitung des Detailbildes. Der HTML-Code für das oben gezeigte Detailbild enthält, wie erwähnt, eine iframe-Angabe:

<iframe width="100%" height="82%" src="/sap/bc/ztrfc/9243835420784A3F3C7A0C2E/output.pdf"/>
Die fett hervorgehobene URL, die an den Requestbehandler für /sap/bc/ztrfc gesendet wird, wird zur Parametrisierung des Requests verwendet: Ab dem Knoten ztrfc enthält die URL nor noch virtuelle Knoten, die nicht "wirklich" im ICF-Servicebaum ausgeprägt sind, sondern dynamisch vom Requestbehandler ausgewertet werden. Es sind dies die Transaktions-ID und der gewünschte Ausgabetyp (PDF). Die URL sieht aus wie die URL eines physischen Dokuments. Das ist beabsichtigt, da wir bemerkt haben, dass die File Extension einer solchen URL vom Internet Explorer ausgewertet wurde, um das korrekte Werkzeug zur Darstellung des Dokuments zu ermitteln.

Hier ist der zuständige Requestbehandler:

method if_http_extension~handle_request .

*---------------------------------------------------------------------
* Syntax der URL
*---------------------------------------------------------------------
*
* /sap/bc/ztrfc/[TID]/output.[Format]
* Daten zur [TID] einlesen und ggf. in [Format] konvertieren
*
* /sap/bc/ztrfc/175A....2B/output.txt
* /sap/bc/ztrfc/175A....2B/output.htm(l)
* /sap/bc/ztrfc/175A....2B/output.csv
* /sap/bc/ztrfc/175A....2B/output.pdf
*
* Im Fehlerfall erfolgt ein redirect auf
* /sap/bc/ztrfc/zsrs123/error.txt
* Dabei ist zsrs123 der T100-Error Code (SE91)
*---------------------------------------------------------------------

  data: lv_path   type string,
        lv_code   type string,
        lv_doc    type string,
        lv_method type z_trans_method.

  lv_path = server->request->get_header_field( name = '~path' ).

* Anhand des Pfadnamens ermitteln, ob Output oder Log zu beschaffen ist
  if lv_path cp '/sap/bc/ztrfc/*'.
    split lv_path+14 at '/' into lv_code lv_doc.
    if lv_doc cp 'output.*'.
      split lv_doc at '.' into sy-msgli lv_method.
      send_reply( exporting io_server  = server
                            iv_id      = lv_code
                            iv_method  = lv_method ).
    elseif lv_doc eq 'log'.
      show_log( io_server    = server
                iv_extnumber = lv_code ).
    elseif lv_doc eq 'Log'.
      show_log( io_server    = server
                iv_lognumber = lv_code ).
    else.
* Syntaxfehler in Pfadangabe '&1'    
      message e009(ztrfc) with lv_path into sy-msgli.
      send_error( server ).
    endif.
  else.
* Die URL &1 ist ungültig, der Pfad beginnt nicht mit &2  
    message e001(ztrfc) with lv_path '/sap/bc/ztrfc/' into sy-msgli.
    send_error( server ).
  endif.

endmethod.

Ein Fortschrittsanzeiger

Manchmal ist keine der oben aufgeführten Lösungen einsetzbar. Das heisst im Grunde genommen, dass man vor den Performanceproblemen kapituliert hat und nun nach einem einfachen Workaround sucht, um im aktuellen Projekt seinen Kopf zu retten. In diesem - schlechtesten - Fall sind Sanduhren oder Fortschrittsanzeiger eine Option. Man beachte aber, dass es sich bei diesen Wegen um Scheinlösungen handelt. Das eigentliche Problem wurde nicht angegangen, stattdessen wird der Benutzer mit einem optischen Trick hinters Licht geführt – wird er doch fälschlicherweise glauben gemacht, dass die langen Antwortzeiten auf seinen Request unvermeidlich sind.

Einfache "Lösungen"

Eine einfache Lösung, um dem User bildlich vorzuführen, dass das System gerade arbeitet, ist ein animiertes GIF wie das bekannte Stundenglas. Ein animiertes GIF ist hierbei sinnvoller als ein statisches Bild, denn die Rechenleistung des Clients können Sie ja nach Belieben verwenden, um die Aktivität des Servers darzustellen. Wenn Sie eine zentrale Funktion haben, durch die alle Ihre Requests geleitet werden (z.B. in unserem BSP-Framework die Funktion generic_submit(), können Sie dort vor dem Absetzen des HTTP-Requests einen zuvor unsichtbaren Bereich in der Webseite sichtbar machen, der ein solches Stundenglas enthält. (Theoretisch wäre auch die Anzeige in einem Popup möglich, aber mit Popups sollte sparsam umgegangen werden, sie werden jedem Benutzer schnell lästig.) Sobald die Antwort zum Requests eintrifft, wird die aktuelle Webseite vollständig überschrieben, und somit verschwindet auch das Stundenglas. Von Lösungen wie dieser hat das SAP-System seinen verballhornten Namen Sanduhr-Anzeige-Programm.

Leider hat diese Lösung den Nachteil, dass der Browser-Timeout nicht ausgehebelt wird: Browserhersteller haben einen Timeoutparameter für Requests. Ist die Antwort nach einer maximal zumutbaren Antwortzeit nicht eingetroffen, gilt der Request als unbeantwortet. Diese maximal zumutbare Antwortzeit liegt üblicherweise bei 30 Sekunden und ist keineswegs aus der Luft gegriffen. Wenn Sie ihren Benutzern noch längere Antwortzeiten abverlangen wollen, müssen Sie schon etwas tiefer in die Trickkiste greifen: Ein unsichtbares Seitenelement – ein iframe oder ein JavaScript-Objekt – müssen damit beauftragt werden, periodisch Requests an das Backendsystem zu versenden. Diese Requests – die an irgendeinen trivialen Service gerichtet sein können, zum Beispiel an /sap/bc/ping – gaukeln dem Browser vor, dass sich auf der Seite ein reges Leben abspielt und halten ihn davon ab, auf die standardmässige Timeoutseite für zu langsam bediente Requests zu wechseln. Für Business Server Pages habe ich diese Lösung (mit einem iframe) in dem BSP-Element keepAlive gekapselt.

Konzept der Fortschrittsanzeige

In gewissen Fällen empfielt sich der Einsatz eines Fortschrittsanzeigers (progress indicator). Ein Fortschrittsanzeiger informiert sich regelmässig im Backend über den Bearbeitungsstand und stellt diesen graphisch dar. Das ist besonders dann sinnvoll, wenn das System eine Arbeitsvorratsliste abzuarbeiten hat. Im SAP GUI haben Sie vielleicht schon für einen ähnlichen Zweck den Funktionsbaustein SAPGUI_PROGRESS_INDICATOR verwendet. In einer Webanwendung muss man natürlich anders vorgehen.

Ich will hier einen Fortschrittsanzeiger auf Basis des JavaScript-Frameworks Prototype in allen nötigen Details erläutern – nicht etwa weil ich besonders begeistert von der Funktionalität des Fortschrittsanzeigers wäre, sondern weil sich an diesem Beispiel gut zeigen lässt, wie bei Business Server Pages clientseitige JavaScript-Logik zur Ergänzung serverseitiger Logik eingesetzt werden kann.

Die Visualisierung

Zuerst müssen Sie sich eine graphische Visualisierung für den Fortschritt ausdenken. Eine einfache Möglichkeit wäre ein Fortschrittsbalken: Ein Rechteck zur Anzeige des Füllstandes. Dies können Sie am einfachsten durch zwei ineinander geschachtelte, formatierte div-Bereiche erreichen:
<div id="progressContainer">
  <div id="progress"></div>
</div>
Die äussere Box formatieren wir mit weissem Hintergrund und dünnem schwarzem Rahmen. Der inneren Box mit der ID progress geben wir eine grüne Hintergrundfarbe. Anfangs soll man jedoch nichts von ihr sehen - ihre statische width ist auf 0 % gesetzt. Ihre height ist 100%, damit sie sich ganz in die äussere Box einfügt. Durch dynamische Manipulation der CSS-Eigenschaft width kann dann die Länge der grünen Box verändert werden, um den Abarbeitungsstand zu visualisieren. Um zu testen, dass die Stilangaben vom Browser wie gewünscht interpretiert werden, setze ich die width temporär auf einen positiven Wert, z.B. 50%. Der progressContainer wird dann zur Hälfte mit grüner Farbe ausgefüllt - wie erwartet.
<style type="text/css">
  #progressContainer {
    background-color:white;
    width:200px;height:20px;
    border:solid black 1px;  
    }
  #progress {
    background-color:green;
    width:0%;height:100%;    /* Test: width:50%;height:100%;  */
    }
</style>
Nun müssen wir uns ein geeignetes Format für den Abarbeitungsgrad ausdenken: Der Server soll uns die Anzahl der bearbeiteten Objekte und deren Gesamtzahl in Form von zwei mit einem '/' getrennten Ganzzahlen senden, etwa so:
17/127
Dieses Format legt also die Schnittstelle zwischen Client und Server fest. Der folgende reguläre Ausdruck stellt offensichtlich einen Validator für dieses Format dar:
/(\d+)\/(\d+)/
Legt man dem JavaScript-Matcher für reguläre Ausdrücke einen Text im gültigen Format vor, gibt er ein Array mit drei Elementen zurück: Das erste Element enthält den gesamten Treffer, die nachfolgenden Elemente enthalten die geklammerten Ausdrücke. Aus diesen lässt sich der prozentuale Fortschrittsgrad errechnen. Mit diesem Wissen können wir eine Funktion schreiben, um einen gegebenen Abarbeitungsgrad zu visualisieren:
function update_status( iStatus ) {
  var lPercent = 100,   // Default 100%, damit ungültige Statustexte als erledigt gelten 
      lMatch = iStatus.match( /(\d+)\/(\d+)/ );
  if (lMatch && lMatch.length == 3 && lMatch[2] > 0 ) {   // iStatus gültig und Nenner > 0
    lPercent = Math.round( lMatch[1] * 10000 / lMatch[2] ) / 100;  // Auf zwei Nachkommastellen runden
    $("progress").style.width = lPercent + "%";
    }
  return lPercent;         
  }
Beachten Sie, dass wir die errechnete Prozentzahl an den Aufrufer zurückgeben und ungültige Statustexte durch Rückgabe von 100 als erledigt betrachten. Auch diese Funktion kann getestet werden. Hierzu benutze ich die Adresszeile des Browsers durch Voranstellen des Pseudo-Protokolls javascript: als JavaScript-Konsole und füge dem auszuführenden Code den Aufruf void(0); an, um nach Ausführung den Argumentstack des JavaScript-Interpreters zu entleeren. Wenn ich das nicht tue, wird das oberste Element des Argumentstacks in einer neuen (leeren) Seite angezeigt. Die folgende Test-URL muss den auf der Seite befindlichen Fortschrittsbalken zu 40 Prozent füllen:
javascript:update_status( "40/100" );void(0);

Periodische Ausführung

Das nächste ist die periodische Ausführung einer Aufgabe. Hierfür gibt es in Prototype die Klasse PeriodicalExecuter. Da wir die Instanz selbst nicht benötigen, können wir sie instanziieren, ohne sie einer linken Seite (einer Referenzvariablen) zuzuweisen. Im Beispiel - noch ohne den Ajax-Request ans Backend - wird die Instanz in der Funktion start() gebildet, die ihrerseits als onclick-Behandler für den Button "Start" fungiert:
function start() {
// Bei Ausführung zurücksetzen
  update_status( 0 );
  new PeriodicalExecuter( get_status, 0.3 );
  }
Es wird also mit einer Periode von 0.3 Sekunden die Funktion get_status() aufgerufen. Damit diese die Chance hat, zu gegebener Zeit die periodische Ausführung zu beenden, bekommt sie beim Aufruf im ersten Argument die Instanz des PeriodicalExecuter mit. Das Objekt PeriodicalExecuter hat eine Methode stop(), mit der die Ausführungen beendet werden können. Um die Oberfläche zunächst unabhängig vom Backend zu testen, verwenden wir eine globale Variable progress, die wir bei jedem Aufruf um 10 erhöhen. Wenn der Wert 100 erreicht ist, werden die Ausführungen gestoppt.
var progress = 0;    // Globale Variable für den UI-Test
function get_status( pex ) {
  progress += 10;    // In Zehnerschritten bis 100 hochzählen
  update_status( progress+"/100" ) < 100 || pex.stop();
  }
Der Oder-Operator || wird hier für eine kompakte Notation eines bedingten Funktionsaufrufs verwendet. Dieses in Perl gänge Idiom (open $file or die) ist auch in JavaScript einsetzbar. Das hervorgehobene Statement liest sich demnach so: "Aktualisiere die Statusanzeige und beende den Periodical Executer, sobald der Fortschrittsgrad von 100% erreicht ist". Das funktioniert so, weil die Funktion update_status() ja die berechnete Prozentzahl zurückgibt. Das Ergebnis sieht dann so aus (drücken Sie den Start-Button):

Start

Der Ajax-Request

Das User Interface ist somit bereits fertig und funktionstüchtig. Nun muss es ans Backend angebunden werden – auch dies ist nicht schwierig. Wir müssen uns vorstellen, dass auf dem Server bereits der Hauptprozess läuft, der die laufzeitintensive Arbeit verrichtet. Dieser schreibt seinen Arbeitsfortschritt regelmässig in einem serverseitigen Cookie fort, das für die Fortschrittsanzeige in einem Nebenprozess ausgelesen wird.

Wir haben oben bereits die Syntax des HTTP-Antworttexts festgelegt. Wir könnten alternativ auch auf dem Server eine JavaScript-Funktion generieren, etwa

update_status( "17/236" );
Wenn man den Content-Type der Antwort auf text/javascript setzt, wird dieser Code bei Eingang der HTTP-Antwort von Prototype automatisch ausgeführt. Das ist natürlich bequem und elegant. Man kann aber auch auf dem Standpunkt stehen, dass die Schnittstelle nicht mit dem vermischt werden sollte, was der Empfänger mit der erhaltenen Information macht. Es erhöht die Wiederverwendbarkeit der Komponente, wenn man die Schnittstelle auf die reine vom Quellsystem angebotene Information reduziert. Das ist der Grund, warum wir für dieses Beispiel eine rein textförmige Schnittstelle gewählt haben, die nur die beiden Rohdaten - aktuelle Dokumentzahl und Gesamtzahl - enthält.

Den Server mit Ajax.Request kontaktieren

Der PeriodicalExecutor muss nun den anzuzeigenden Arbeitsfortschritt mittels eines Requests an den Server ermitteln und die Statusanzeige aktualisieren. Wir brauchen also zunächst einmal einen Requestbehandler auf dem Server.

Dieser Requestbehandler - eine Art Servlet auf ABAP - ist eine winzige Klasse, die wir als erstes im ICF-Servicebaum hinterlegen. Dadurch ordnen wir ihm eine URL zu, unter der er aufgerufen werden kann. In unserem Beispiel wählen wir den Pfad /zprogress und ordnen ihm eine Klasse zcl_progress_status zu. Diese muss, um als Requestbehandler fungieren zu können, das Interface if_http_extension implementieren. Das Interface besteht aus der einzigen Methode handle_request() (neben einigen Konstanten). Das ist auch logisch: das mindeste, was in einem Requestbehandler programmiert sein muss, ist seine Art, einen Request zu beantworten!

Der Fortschrittsanzeiger kann dann mit der Klasse Ajax.Request des Prototype-Frameworks sehr einfach - und vom konkreten Browser unabhängig - aufgerufen werden. Die Funktion get_status(), die wir schon oben diskutiert haben, muss nun wie folgt geändert werden:
function get_status( pex ) {
  new Ajax.Request( '/zprogress', {
     onSuccess:function(r) {
       update_status( r.responseText ) < 100 || pex.stop();
       },
     parameters:{
       sap_appcontext:get_appcontext()
       }
     }
   );
 }
Das erste Argument des Konstruktors ist die URL, an die der Request gerichtet sein soll. Aus Sicherheitsgründen muss dies immer ein Service mit derselben Domäne sein. Wir können die Domäne also auch weglassen und nur den Pfad des Servicebehandlers angeben (wie hier). Das zweite Argument ist ein Hash, der es erlaubt, den Request geeignet zu parametrisieren. Der Hash ist also eigentlich eine Sammlung weiterer benannter Argumente, deren Namen und Bedeutungen der Prototype-Dokumentation entnommen werden können.

Da wir die Instanz des Requests zu dem Zeitpunkt, wenn wir ihn absetzen, nicht mehr explizit benötigen, lassen wir - wie beim PeriodicalUpdater - die linke Seite der Zuweisung einfach weg, schreiben also nicht r = new Ajax.Request(...), sondern einfach new Ajax.Request(...). Die Syntax von JavaScript lässt das zu. Implizit merkt sich das Framework die Instanz aber trotzdem. Denn wenn der asynchrone Response eingeht, soll ja die für das Ereignis onSuccess registrierte Funktion ausgeführt werden. Diese Funktion bekommt, wie oben der PeriodicalExecuter, implizit im ersten Argument die Instanz des Ajax.Request-Objekts übergeben. Wenn wir den ersten Funktionsparameter in der Deklaration des Behandlers benennen, können wir die Instanz im Funktionsrumpf unter diesem Namen verwenden (hier r).

Die ID der BSP-Session

Zum Schlüsselwort parameters übergeben wir Requestparameter. Ajax.Request versendet sie je nach der gewählten Requestmethode als Get- oder Postparameter. Hier haben wir die Methode nicht spezifiziert (was wir mit dem Schlüsselwort method tun müssten), daher wird der Default - post - verwendet. In diesem Fall übergeben wir nur den Parameter sap_appcontext, der erst beim Ausführen des Konstruktors durch Aufruf der Funktion get_sap_appcontext() evaluiert wird.

Was hat es nun mit diesem Parameter sap_appcontext auf sich? Wir benötigen ihn zur Kennzeichnung der Sitzung. Denn jeder Benutzer will ja nur seinen eigenen Fortschritt angezeigt bekommen, nicht den seines Kollegen. Also muss der Fortschrittsanzeiger eine Möglichkeit haben, die Sitzung zu identifizieren, für die er aufgerufen wird. Es ist ja auch die Sitzung, deren serverseitiges Cookie er einlesen muss. Eine Browserinstanz, in der eine BSP-Seite aufgerufen wird, verfügt auf dem Client wie auf dem Server immer über eine sap-sessionid, die die Sitzung eindeutig kennzeichnet. Wenn im Browser Cookies aktiviert sind - wovon wir einmal ausgehen wollen - ist die session-id zusammen mit allfälligen weiteren Parametern in einem nichtpersistenten Cookie namens sap-appcontext vorzufinden. Eine Sitzung mit BSP enthält daher unter ihren Cookies ein Feld namens sap-appcontext:

Wenn Sie sich fragen, was sich hinter dem seltsamen Wert des Cookiefelds sap-appcontext verbirgt: Sie gehen recht in der Annahme, dass es sich um einen base64-codierten Wert handelt. Wenn Sie ihn mit irgendeinem base64-Decoder entziffern (die es in grosser Zahl im Internet gibt) und danach noch ein unescape ausführen, erhalten Sie zum Beispiel folgenden Inhalt:

sap-sessionid=SID:ANON:migzmc04_D12_01:zZhBttOb0tgIkQAVn-yDL2uoabhZ7X5zDN1_Lff3-ATT
Im sap-appcontext verbirgt sich also die SAP-Session-ID! Da das Prototype-Framework keinen base64-Decoder enthält und wir nicht extra einen JavaScript-Decoder in die Webseite einbauen wollen, um das Feld sap-appcontext auszupacken, schicken wir es so wie es ist, an den Server und erledigen die Decodierung dort. Wir erhalten dann exakt denselben Wert, der auch in der BSP-Applikation (also im Hauptprozess, der die langsame Operation ausführt) unter runtime->session_id verfügbar ist und der von den Convenience-Methoden set() und get() der Klasse cl_bsp_server_side_cookie implizit verwendet wird. Auf dem Client codieren wir daher die Funktion get_sap_appcontext() wie folgt:
function get_appcontext() {                                                                
  var lAppcontextCookie = document.cookie.match(/sap-appcontext=(.*?)(?:;|$)/);            
  return lAppcontextCookie &&                                                      
         ( lAppcontextCookie.length >= 2) &&                                               
         unescape( lAppcontextCookie[1] ) || "";                                           
  }                                                                                        

Der onSuccess-Behandler

Kommen wir nun zum Behandler der HTTP-Antwort, den wir als anonyme Funktion hingeschrieben haben:
function get_status( pex ) {
  new Ajax.Request( '/zprogress', {
     onSuccess:function(r) {
       update_status( r.responseText ) < 100 || pex.stop();
       },
     parameters:{
       sap_appcontext:get_appcontext()
       }
     }
   );
 }
Als erstes Argument wird beim Aufruf eines Ajax-Ereignisbehandlers immer die Instanz des Ajax.Requests selbst übergeben. Diese benötigen wir, um die HTTP-Antwort auszuwerten. Das Attribut responseText enthält nach unserer Konvention den Abarbeitungsstand, zum Beispiel 23/157. Der Funktionsrumpf der anonymen Funktion, die das Event onSuccess sollte Sie nicht weiter überraschen. Wie oben in der Funktion set_status() wird das UI aktualisiert und am Schluss der PeriodicalExecuter beendet - nur kommt der Statusstring nun vom Backend.

Etwas sollte Sie aber doch verwundern. Das erste A in Ajax steht bekanntlich für asynchronous. Das bedeutet, dass der onSuccess-Behandler zu einem nicht kontrollierbaren Zeitpunkt ausgeführt wird, nachdem die Funktion get_status() längst verlassen wurde. Wieso kennt die Funktion dann immer noch das Symbol pex, das ja als Importparameter der Funktion get_status nur in deren lokalen Kontext definiert ist?

Die Antwort ist: Weil das in JavaScript eben so ist - aber ein bisschen sollte man sich trotzdem darüber wundern! Hier liegt eine sogenannte Closure vor: Wenn wie hier in der Implementierung einer lokal deklarierten Funktion Symbole des lokalen Kontexts verwendet werden (hier pex) und die Referenz auf diese Funktion den aktuellen Aufruf überlebt (indem sie zum Beispiel an ihren Aufrufer übergeben oder, wie hier, einem anderen, länger lebenden Objekt als Behandler vorgelegt wird), bewahrt die JavaScript-Laufzeit den gesamten lokalen Kontext auf und stellt ihn, wenn die Funktion ausgeführt wird, für deren Gebrauch wieder her.

Das Template-Objekt

Kommen wir zurück zur Anzeigefunktion. Wir wollen sie nun noch um einen Statustext anreichern, etwa "16 von 123 Dokumenten abgearbeitet". Mit dem Template-Objekt von Prototype können wir das in gut lesbarer Form ausdrücken. Wir hätten natürlich auch zweimal die native JavaScript-Funktion String.replace() aufrufen können, aber das Prototype-Template verbessert meines Erachtens die Lesbarkeit des Codes:
// Template für Statusanzeige global einführen, um es nicht immer neu instanziieren zu müssen
       var gStatusTemplate = new Template(
         "#{actual} von insgesamt #{total} Dokumenten bearbeitet"
         );

// Aktualisiert anhand des Response die Statusanzeige
// Rückgabewert: Abarbeitungsgrad in Prozent
       function update_status( iStatus ) {
         var lPercent = 100,  // für ungültige Antworten "Erledigt" zurückgeben
             lMatch   = iStatus.match( /(\d+)\/(\d+)/ );
         if (lMatch && lMatch.length == 3 && lMatch[2] > 0) {
           lPercent = Math.round( lMatch[1] * 10000 / lMatch[2] ) / 100;  // Prozentzahl errechnen, auf 2 Stellen runden
           if (lPercent > 100) lPercent = 100;
           $("box").style.width = lPercent + "%";
           $("progressText").update(
             gStatusTemplate.evaluate(
                { actual : lMatch[1] ,
                  total  : lMatch[2]} )
                );
           }
         return lPercent;
         }

Die Implementierung von ZCL_PROGRESS_STATUS

Wie geht es nun auf dem Server weiter? Der Requestbehandler muss zunächst die Session-ID aus dem Formularfeld sap_appcontext entpacken. Danach muss er das Fortschrittscookie lesen, das vom Hauptprozess geschrieben wurde und den Status in der Form [aktuell]/[total] ermitteln. Dieser wird dann im text/plain-Format zurückgesendet:

method if_http_extension~handle_request.

  data: lv_status         type string,
        lv_session_id     type string.

  lv_session_id = get_session_id( server->request ).

  lv_status = get_status( lv_session_id ).

  server->response->set_cdata( lv_status ).
  server->response->set_header_field( name = 'Content-Type'
                                      value = 'text/plain' ).

endmethod.
Die Methode get_session_id kann dabei auf den in der Klasse cl_http_utility implementierten base64-Decoder zurückgreifen:
method get_session_id.

  data: lv_app_context   type string,
        lv_app_context64 type string.

* Formularfeld einlesen, das vom Client mittels JavaScript gefüllt wurde
  lv_app_context64 = io_request->get_form_field( 'sap_appcontext' ).

* Applikationskontext decodieren
  lv_app_context   = cl_http_utility=>decode_base64( lv_app_context64 ).

* Session-ID extrahieren
  find regex 'sap-sessionid=([^;]+)(?:;|$)'
       in lv_app_context
       submatches ev_session_id.

  ev_session_id = cl_http_utility=>unescape_url( ev_session_id ).

endmethod.
Entscheidend für die darauf folgende Methode get_status(), die nun den Status dieser Session ermittelt, ist die Beschaffung des Cookies, in dem die Hauptapplikation ihren Bearbeitungsstand fortschreibt. Dies muss nun mit der Methode get_server_cookie geschehen, da nur in dieser Methode (anders als in der Convenience-Methode get()) die Session-ID als Parameter angegeben werden kann:
  call method cl_bsp_server_side_cookie=>get_server_cookie
    exporting
      name                  = 'progress'
      application_name      = 'zdemo'
      application_namespace = 'z'
      username              = sy-uname
      session_id            = iv_session_id
      data_name             = 'dummy'
    changing
      data_value            = lv_status.
Je nachdem, wie die Hauptapplikation das Feld lv_status fortschreibt, ist danach noch ein Mapping in die gewünschte Syntax des Servlet-Antwortstrings nötig.

Das Ergebnis können Sie sich auf der Beispielseite zum Buch unter Fortschrittsanzeige ansehen. Das Beispiel führt alle oben aufgeführten Schritte, arbeitet auch mit einem serverseitigen Cookie. Aber anders als in einem echten Beispiel, wird dieses Cookie nicht von einem ressourcenintensiven Hauptprozess aktualisiert. Das Session-Cookie enthält stattdessen die Uhrzeit, als /zprogress in dieser Session erstmalig aufgerufen wurde. Der Fortschrittsstatus wird dann mit einer Parabel simuliert (mit fortschreitender Zeit wird die "Abarbeitung" also schneller), die nach einer vorgegebenen Zeitdauer eine vorgegebene "Gesamtzahl von Belegen" erreicht.

Änderungen für den produktiven Einsatz

In einem echten, produktiven Anwendungsfall würden Sie nur die Funktion start() zu ändern haben. Sie müsste die Fortschrittsanzeige zunächst sichtbar machen (sie sollte, bevor der Benutzer den kritischen Button drückt, unsichtbar in der Seite enthalten sein), dann den PeriodicalExecuter wie oben instanziieren und danach noch den genericSubmit() ausführen, um die BSP-Applikation zur Ausführung der Operation zu veranlassen:
function start() {
// Fortschrittsanzeige sichtbar machen
  $("progressContainer").style.display = "block";

// Periodische Abfrage starten
  new PeriodicalExecuter( get_status, 0.7 );

// Selektion ausführen
  genericSubmit( 'main__select' );
  
  }
Wenn Sie es so machen, brauchen Sie sich um das Abräumen des Fortschrittsanzeigers nicht zu kümmern: Wenn die Selektion beendet ist, erhält die Browserseite einen vollständig neuen HTML-Content, der die bestehende Seite mitsamt aller darin eingebauten dynamischen Features überschreibt.

Zurück