In SAPUI5 oder OpenUI5 sind Modelle eine einfache Möglichkeit, um die Datenhaltung von der Darstellung zu entkoppeln (nach dem MVC Muster). Dafür übernimmt das Modell die Kommunikation mit der Datenquelle, während die grafischen Elemente (Controlls) über das Binding mit dem Modell verknüpft sind. Die Aktualisierung der Daten/Eigenschaften der Controlls erfolgt event-basiert und damit automatisch.

Die Datenquelle für das Modell ist dabei üblicherweise ein Webservice, zum Beispiel ein OData-Service. Auch lokale Dateien können verwendet werden, allerdings nur lesend. Diese Einschränkung resultiert daraus, dass eine UI5-Anwendung mittels Javascript im Browser läuft, und damit keine Daten auf den Server schreiben kann.

Für eine kleine (interne) Anwendung kann es aber nützlich sein, dass die Daten lokal in Dateien gespeichert werden können. Dazu soll folgendes eingerichtet werden:

  • Die Hauptkomponente (Component.js) der App erhält die Funktionen zum Lesen und Schreiben des Modells, inklusive eines Caches, um die Datenmenge zu reduzieren.
  • Ein lokales PHP-Skript, das über Ajax aufgerufen wird, übernimmt das Speichern der Dateien.
  • Eine .htaccess-Datei soll den Zugriff auf das Dateisystem einschränken.

Die Hauptkomponente

Die Component.js erweitern wie um den folgenden Code:

init: function() {
    UIComponent.prototype.init.apply(this, arguments);
    this._oDataCache = {};
  },
  loadFile: function(sFilename, oModel) {
    if (this._oDataCache[sFilename]) {
      oModel.setJSON(this._oDataCache[sFilename].Data);
      return;
    } else {
      oModel.loadData(sFilename, "", false, "Get", false, false);
      this._oDataCache[sFilename] = {
        "Data": oModel.getJSON()
      };
      return;
    }
  },
  _startQueue: function() {
    this._queueRunning = true;
    this._saveNextFile();
  },
  saveFile: function(sData, sFilename) {
    this._oDataCache[sFilename] = {
      "Data": sData,
      "Updkz": "X"
    }
    if (!this._queueRunning) {
      this._startQueue();
    }
  },
  _saveNextFile: function(self = this) {
    if (!self._oDataCache) {
      self._queueRunning = false;
      return;
    }
    self._foundFileWithUpdate = false;
    for (var element in self._oDataCache) {
      if (self._oDataCache[element].Updkz == "X") {
        self._oDataCache[element].Updkz = "";
        self._sendFile(
          self._oDataCache[element].Data,
          element
        );
        self._foundFileWithUpdate = true;
        break;
      }
    }
    if (!self._foundFileWithUpdate) {
      self._queueRunning = false;
    }
  },
  _sendFile: function(sData, sFilename) {
    var data = {
      "data": sData,
      "filename": sFilename
    };
    parent = this;
    $.ajax({
      type: "POST",
      url: "./savefile.php",
      data: data,
      error: function(e, s) {
        console.warn(s);
      },
      success: this._saveNextFile(parent),
    });
  },

Die Funktionen, die aus den Controllern heraus aufgerufen werden, lauten nun

  • this.getOwnerComponent().loadFile(sFilename, oModel)
    • hierbei ist sFilename ein String mit dem Dateinamen und
    • oModel ein sap.ui.model.json.JSONModel() Objekt
  • this.getOwnerComponent().saveFile(sData, sFilename)
    • sData sind die Daten als valider JSON String (kann über oModel.getJSON() erhalten werden)
    • sFilename ist der String mit dem Dateinamen

In der Funktion „loadFile“ wird:

  • zunächst geschaut, ob Daten im Cache vorhanden sind.
  • Falls keine Daten im Cache sind, dann werden die Daten vom Dateisystem mit der UI5-Funktion oModel.loadData() gelesen. Die Parameter sind:
    • Dateiname als String
    • Parameter (leerer String, da keine weiteren Parameter mitgegeben werden müssen)
    • Das Laden erfolgt synchron (aSync = false), damit die Daten wirklich zur Verfügung stehen, bevor eine weitere Verarbeitung stattfindet.
    • Der Typ ist “GET” (“POST” ist auch möglich)
    • Merge = false (Daten werden ersetzt)
    • Cache = false (forciert das Laden ohne UI5-Model Cache, da eigener Cache verwendet wird)
  • Anschließend werden die Daten zum eigenen Cache hinzugefügt. Der eigene Cache kann einfach zurückgesetzt werden, sodass ein Nachladen erzwungen wird.

In der Funktion „saveFile“ werden:

  • die Daten dem Cache hinzugefügt und vermerkt, dass diese auf dem Dateisystem aktualisiert werden müssen (Updkz = ‘X’).
  • Anschließend wird die Verarbeitung als Queue angestoßen, falls die Queue aktuell nicht läuft. Eine Verarbeitungsqueue sorgt dafür, dass nicht mehrere Vorgänge gleichzeitig auf das System schreiben.

Solange noch Daten im Cache sind, die das Updatekennzeichen besitzen, ruft die Queue rekursiv die Funktion “_saveNextFile” auf. Um zu verhindern, dass gleichzeitig mehrere Zugriffe auf die selbe Datei erfolgen, erfolgt der nächste Aufruf erst in dem “success”-Hook des Ajax-Requests.

  • Der Ajax-Aufruf erfolgt für die URL “./savefile.php”,
  • die Daten (JSON-Inhalt und Dateiname) werden per POST-Methode an das Skript übergeben

Minimales Server-Skript zum Speichern

In der savefile.php könne nun noch diverse Berechtigungsprüfungen auftauchen, sodass ein nicht-autorisiertes Schreiben verhindert werden kann. Hier wird davon ausgegangen, dass es sich um eine interne Anwendung handelt, deren Zugriff per htaccess beschränkt wird.

<?php
$data     = $_POST['data'];
$filename = $_POST['filename'];
if ($data == '') {
  echo "No Data";
} else {
  if ($filename == '') {
    $filename = 'localService/test.json';
  }
  $dirname = dirname($filename);
  if (!is_dir($dirname)) {
    mkdir($dirname, 0755, true);
  }
  $f = fopen($filename, 'w+');
  if ($f) {
    fwrite($f, $data);
    fclose($f);
    echo "Write to $filename\n" . $data;
  } else {
    echo "Fehler beim oeffnen der Datei $filename";
  }
}
?>

Das Skript legt neben der Datei auch das Verzeichnis an, falls dieses noch nicht existiert.

Zugriffsbeschränkung auf das interne Netz

Um den Zugriff zu beschränken legt man eine .htaccess-Datei mit dem folgenden Inhalt an:

SetEnvIf remote_addr ^192.168.*.* allowedip=1
Order deny,allow
deny from all
allow from env=allowedip

Diese Datei erlaubt den Zugriff auf die Webseite nur, wenn man eine lokale IP aus dem Bereich “192.168.*.*” besitzt.