RAP - Übersetzungs-App (Beispiel)
Schauen wir uns einmal ein praktisches Beispiel für die Entwicklung einer RAP Anwendung im ABAP Environment an und wie du mit wenig Aufwand eine App erstellst.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns einmal ein praktisches Beispiel für eine Umsetzung in RAP an, dabei werden wir auf verschiedene Dinge des Frameworks eingehen und wie es uns hilft, einfache Anwendungen zu entwickeln.
Einleitung
In der Vergangenheit hatten wir uns bereits die Integration der Google Translate API angeschaut und wie du diese einfach aus ABAP heraus aufrufen kannst. In diesem Beispiel schauen wir uns einmal eine RAP Integration an und wie du die API nutzen kannst, um Texte und Wörter nachhaltig zu übersetzen.
Tabelle
Um unsere Anwendung aufbauen zu können, benötigen wir im ersten Schritt ein Datenmodell. Dazu definieren wir die folgende Tabelle, die den zu übersetzenden Text und den Zieltext beinhaltet. Dabei verwenden wir den ISO-Code, um diesen einfacher an die Schnittstelle zu übergeben. Neben den Feldern für das Datenmodell, sind auch technische Felder für den Draft enthalten. Den Schlüssel lassen wir vom Framework automatisch generieren und setzen deshalb auf eine UUID.
define table zbs_dmo_lang {
key client : abap.clnt not null;
key identification : sysuuid_x16 not null;
source_language : laiso;
source_text : zbs_demo_text;
target_language : laiso;
target_text : zbs_demo_text;
local_created_by : abp_creation_user;
local_last_changed_by : abp_locinst_lastchange_user;
local_last_changed : abp_locinst_lastchange_tstmpl;
last_changed : abp_lastchange_tstmpl;
}
Das Datenelement ZBS_DEMO_TEXT ist ein CHAR 500 Feld mit Groß-/Kleinschreibung, um darin die Texte für uns abbilden zu können.
RAP Generator
Zur Erzeugung des RAP Stacks verwenden wir den RAP Generator in den ABAP Development Tools, da wir nur eine Entität haben und damit die etwas größere Konfiguration in der Fiori Version entfällt. Den RAP Generator startest du über das Kontextmenü im "Project Explorer" unter dem Punkt "Generate ABAP Repository Objects ...":
Wir möchten einen "OData UI Service" erstellen und wählen diesen Punkt im Generator. Im nächsten Schritt befüllen wir die einzelnen Punkte und können die Namen der zu generierenden Objekte noch einmal anpassen. Sind wir mit dem Ergebnis zufrieden, erhalten wir im nächsten Schritt die Übersicht der Objekte die zu generieren sind. Nach der Erstellung sollten wir die folgenden Objekte in unserem Paket finden. Die Tabelle und die Google Integration gab es bereits vor dem Generieren der Objekte.
Um den Service nun zu testen, müssen wir diesen über den "Publish" Button im Service Binding noch aktivieren, im Anschluss können wir uns den Preview anschauen. So weit ist die App funktional, es fehlt allerdings noch einiges an Optimierungen.
Die Detailseite enthält alle Felder, aber auch Informationen wie den technischen Schlüssel oder organisatorische Informationen, die wir dem Anwender nicht zeigen möchten.
Hinweis: Grundsätzlich empfehlen wir dir im ersten Schritt den Stack und den Aufbau von RAP Anwendungen manuell zu lernen und die einzelnen Schichten zu verwenden, bevor du dir mit dem Generator Anwendungen erstellst. Damit erzielst du die besten Ergebnisse beim Lernen vom ABAP RESTful Programming Model.
Anpassung UI
Im nächsten Schritt wollen wir die UI etwas anpassen und aufräumen. Dazu möchten wir die Suche anpassen, die Felder für die Selektion einschränken, die nötigsten Informationen in der Liste anzeigen und das Detailbild aufräumen.
Selektion
Im ersten Schritt wollen wir die Felder der Selektion einschränken, hier reicht uns Quell- und Zielsprache. Über die Annotation "UI.selectionField" können wir das Feld in der FilterBar anzeigen. Die Position bestimmt, ob das Feld weiter vorn auftaucht, wenn die Nummer niedrig ist. Die Positionsnummer kann frei vergeben werden.
@UI.selectionField: [ { position: 10 } ]
SourceLanguage;
Ausblenden
Um unnötige Felder (Schlüssel und technische Informationen) auszublenden, verwenden wir die Annotation "UI.hidden". Hier ein Beispiel für die Identifikation:
@UI.hidden: true
Identification;
Liste
Wollen wir eine Spalte in der Liste einblenden, verwenden wir die Annotation "UI.lineItem". Wie bei der Selektion verhält es sich auch hier mit der Position, die Reihenfolge ergibt sich aus den Nummern, beginnend mit der niedrigsten Nummer. Die Eigenschaft "importance" setzen wir bei den Texten auf "high". In der Responsive Tabelle sorgt es dafür, wenn nicht mehr genug Platz zur Verfügung steht, dass die Felder mit der höchsten Wichtigkeit angezeigt werden. So können wir die sichtbaren Informationen für den Anwender steuern.
@UI.lineItem: [ { position: 30, importance: #HIGH } ]
SourceText;
Die Sprachen lassen wir auf Default und setzen dafür die Texte auf "high", damit diese immer sichtbar sind.
Überschrift
Die Überschrift der Detailseite wollen wir anpassen und anstatt der UUID den Text der Quellsprache anzeigen. Dazu entfernen wir die Beschreibung und tauschen das Element in der Annotation "UI.headerInfo.title.value".
@Metadata.layer: #CORE
@UI.headerInfo.title.type: #STANDARD
@UI.headerInfo.title.value: 'SourceText'
annotate view ZBS_C_DMOLanguage with
Details
Nun wollen wie die Detailseite in zwei Bereiche unterteilen, einmal die Quelle und einmal das Ziel, entsprechend der Text mit der Sprache. Dazu benötigen wir im ersten Schritt eine UI Facet, der wir die Felder zuordnen können. Dazu legen wir die folgenden Facets unterhalb der Definition an:
@UI.facet: [
{
label: 'Source',
id: 'idSource',
purpose: #STANDARD,
position: 10 ,
type: #IDENTIFICATION_REFERENCE,
targetQualifier: 'SOURCE'
},
{
label: 'Target',
id: 'idTarget',
purpose: #STANDARD,
position: 20 ,
type: #IDENTIFICATION_REFERENCE,
targetQualifier: 'TARGET'
}
]
Die "ID" muss auch eindeutig sein, da es sonst zu Problemen beim Rendern der Anwendung kommen kann. Position bestimmt auch hier wieder die Reihenfolge der Elemente. Der "targetQualifier" wird benötigt, um die Elemente einer Facet zuordnen zu können. Am Feld hinterlegen wir per "UI.identification" die entsprechenden Informationen, der "qualifier" übernimmt dann die Zuordnung zur richtigen Facet.
@UI.identification: [ { position: 10, qualifier: 'TARGET' } ]
TargetLanguage;
Langtexte
Für die Eingabe eines Langtextes ist das Feld leider recht klein, hier haben wir die Möglichkeit über die Annotation "UI.multiLineText" ein Langtextfeld zu erzeugen.
@UI.multiLineText: true
TargetText;
Ergebnis
Nachdem wir nun die Metadata Extension angepasst haben, sehen die Annotation nun wie folgt aus:
@Metadata.layer: #CORE
@UI.headerInfo.title.type: #STANDARD
@UI.headerInfo.title.value: 'SourceText'
annotate view ZBS_C_DMOLanguage with
{
@UI.facet: [
{
label: 'Source',
id: 'idSource',
purpose: #STANDARD,
position: 10 ,
type: #IDENTIFICATION_REFERENCE,
targetQualifier: 'SOURCE'
},
{
label: 'Target',
id: 'idTarget',
purpose: #STANDARD,
position: 20 ,
type: #IDENTIFICATION_REFERENCE,
targetQualifier: 'TARGET'
}
]
@UI.hidden: true
@EndUserText.label: 'Identification'
Identification;
@UI.identification: [ { position: 10, qualifier: 'SOURCE' } ]
@UI.lineItem: [ { position: 10 } ]
@UI.selectionField: [ { position: 10 } ]
@EndUserText.label: 'Source Language'
SourceLanguage;
@UI.identification: [ { position: 20, qualifier: 'SOURCE' } ]
@UI.lineItem: [ { position: 20, importance: #HIGH } ]
@UI.multiLineText: true
@EndUserText.label: 'Source Text'
SourceText;
@UI.identification: [ { position: 10, qualifier: 'TARGET' } ]
@UI.lineItem: [ { position: 30 } ]
@UI.selectionField: [ { position: 20 } ]
@EndUserText.label: 'Target Language'
TargetLanguage;
@UI.identification: [ { position: 20, qualifier: 'TARGET' } ]
@UI.lineItem: [ { position: 40, importance: #HIGH } ]
@UI.multiLineText: true
@EndUserText.label: 'Target Text'
TargetText;
@UI.hidden: true
@EndUserText.label: 'Created By'
LocalCreatedBy;
@UI.hidden: true
@EndUserText.label: 'Changed By'
LocalLastChangedBy;
@UI.hidden: true
@EndUserText.label: 'Changed On'
LocalLastChanged;
@UI.hidden: true
@EndUserText.label: 'Changed On'
LastChanged;
}
Die Einstiegsseite und die Liste ist nun aufgeräumt und wir sehen nur die Informationen, die der User zum Arbeiten benötigt.
Auf der Objektseite gibt es nun zwei Bereiche für die Quell- und die Zielsprache mit Text. Dabei können die Texte als Langtext mit mehr Platz gepflegt werden.
Suche
Hier wollen wir die Suche in unserer App verbessern und implementieren Suchen und Wertehilfen an den Feldern.
Suchhilfe
Im nächsten Schritt wollen wir für unsere Sprachen eine Suchhilfe zur Verfügung stellen, dafür verwenden wir die Annotation "Consumption.valueHelpDefinition". Über das Element Entity geben wir den Core Data Service den wir für die Suchhilfe verwenden wollen. In diesem Fall verwenden wir den Standard View I_Language. Über Element mappen wir das Feld aus der Suchhilfe auf unser aktuelles Feld.
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_Language', element: 'LanguageISOCode' } }]
SourceLanguage,
Wenn wir nun die Suchhilfe am Feld öffnen, öffnet sich der View und wir können nach der Sprache suchen.
Fuzzy Suche
Als nächsten Schritt wollen wir bei den Texten eine Suche hinzufügen, die eine gewisse Unschärfe (Fuzziness) zulässt. Der Anwender soll über die beiden Texte (Quell und Ziel) suchen können und wenn Rechtsschreibfehler im Text oder der Suche sind, trotzdem einigermaßen sinnvolle Ergebnisse bekommen. Dazu müssen wir im Header des Projektions-Views die Annotation "Search.searchable" setzen, damit kann in diesem View mit der Freitextsuche gesucht werden.
@Search.searchable: true
define root view entity ZBS_C_DMOLanguage
Nun müssen wir noch Felder definieren, über die mit diesem Feld gesucht werden kann. Dazu können wir die Annotation "Search.defaultSearchElement" verwenden und so die beiden Textfelder für die Suche aktivieren.
@Search.defaultSearchElement: true
SourceText,
Um nun eine Fuzziness zu erreichen, müssen wir noch eine Annotation "Search.fuzzinessThreshold" ergänzen. Hier kannst du einen Wert von 1.0 (genaue Übereinstimmung) bis 0.7 (sehr ungenau) verwenden, was die Suche angeht. Weitere Informationen findest du zur Annotation in der SAP Help.
@Search.fuzzinessThreshold: 0.8
TargetText,
Ergebnis
Wenn wir nun unsere Anwendung aufrufen, dann haben wir ein zusätzliches Suchfeld (Freitextsuche) und können in den definierten Spalten suchen. Wenn wir nun eine Suche mit Rechtschreibfehler durchführen, erhalten wir trotzdem einen Treffer und können mit dem Ergebnis weiterarbeiten.
Der komplette Projektions-View sieht nun wie folgt aus und alle gemachten Erweiterungen sind enthalten (Suche, Wertehilfe).
@Metadata.allowExtensions: true
@EndUserText.label: 'Language (Projection)'
@AccessControl.authorizationCheck: #CHECK
@Search.searchable: true
define root view entity ZBS_C_DMOLanguage
provider contract transactional_query
as projection on ZBS_R_DMOLanguage
{
key Identification,
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_Language', element: 'LanguageISOCode' } }]
SourceLanguage,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
SourceText,
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_Language', element: 'LanguageISOCode' } }]
TargetLanguage,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
TargetText,
LocalCreatedBy,
LocalLastChangedBy,
LocalLastChanged,
LastChanged
}
Hinweis: Ursprünglich haben wir die Texte in der App als String definiert, dieser Typ wird aber nicht bei der Fuzzy Suche unterstützt und es kommt zu einem Dump. Deshalb wurden die Elemente auf Datenelemente vom Typ CHAR geändert.
Übersetzung
In diesem Abschnitt integrieren wir die Übersetzungsfunktionalität in unser Datenmodell, um die Anwendung unserem User zur Verfügung zu stellen.
Aktion
Im ersten Schritt benötigen wir eine Aktion, damit der User für seinen Text die Übersetzung starten kann. Dazu wollen wir keine statische, sondern eine instanzbasierte Aktion anlegen. In der Verhaltensdefinition legen wir die Aktion namens "TranslateText" an die als Ergebnis eine Instanz von sich selbst zurückgibt. Das Ergebnis benötigen wir, damit sich die die UI mit den neuen Daten aktualisiert.
action TranslateText result [1] $self;
Über STRG + 1 (Quick Assist) lassen wir uns die Methode in der Verhaltensimplementierung generieren und könnten direkt mit der Implementierung starten.
Nach der Aktivierung erweitern wir die Projektion der Verhaltensdefinition und geben die neue Aktion zur Nutzung an die App frei. Nicht freigegebene Aktionen können nicht genutzt werden. Wenn wir zum Beispiel die "Create" Aktion nicht freigeben, kann über die App kein neuer Datensatz erzeugt werden, obwohl das RAP Objekt es eigentlich kann.
use action TranslateText;
Damit wir die Aktion nun auch im UI sehen, müssen wir sie in die Metadata Extension aufnehmen. Dabei steht die Identification oder das LineItem zur Verfügung. In beiden Fällen benötigen wir den Typ "FOR_ACTION" und das Element "dataAction", wo wir unsere Aktion aufnehmen. Über das "Label" haben wir dann noch die Möglichkeit dem Button einen Text zu geben.
@UI.identification: [ { position: 10, qualifier: 'SOURCE' },
{ position: 10, type: #FOR_ACTION, dataAction: 'TranslateText', label: 'Translate' } ]
@UI.lineItem: [ { position: 10 },
{ position: 10, type: #FOR_ACTION, dataAction: 'TranslateText', label: 'Translate' } ]
SourceLanguage;
Ist die Aktion als "LineItem" definiert, taucht sie unter der FilterBar und über der Tabelle mit den Daten auf.
Definieren wir die Aktion in der "Identification", so taucht der Button dafür auf der Objektseite neben den anderen Aktionen im oberen Bereich auf.
Implementierung
Nachdem nun die Aktion im UI sichtbar ist, kümmern wir uns um die Implementierung der Logik in der Klasse. Wenn wir Aktionen implementieren, machen wir das grundsätzlich auch in der Annahme, dass mehr als ein Datensatz ausgewählt wurde und wir eine Massenverarbeitung durchführen müssen. Die Methode erhält deshalb eine Tabelle von Schlüsseln und nicht nur einen Eintrag. Dadurch das wir ein Result definiert haben, wird die Methode allerdings x-Mal aufgerufen, jeweils mit einem Datensatz.
Im ersten Schritt lesen wir die zusätzlichen Daten aus den selektierten Entitäten, um an die zu übersetzenden Elemente zu gelangen. Dazu verwenden wir die Entity Manipulation Language (EML) im LOCAL MODE, damit Feature Control und Berechtigungsprüfungen nicht aktiv sind und die Änderung des Objekts immer stattfinden kann.
READ ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_selected).
Im Anschluss verarbeiten wir die Daten in einer Schleife, zumindest dort wo auch eine Zielsprache gesetzt ist. Im ersten Schritt rufen wir die Google Translate API auf und lassen den Text übersetzen.
DATA(ld_translated) = NEW zcl_bs_demo_google_integration( )->translate_text(
id_target_language = to_lower( ls_selected-TargetLanguage )
id_text = CONV #( ls_selected-SourceText ) ).
Im Anschluss rufen wir den MODIFY auf und übergeben den neuen Inhalt an unser Business Objekt. Den Schlüssel können wir über %TKY ansprechen und sparen uns bei mehr Schlüsselfeldern ein aufwändiges Mapping. Wir müssen die Kontrollstruktur setzen, wenn wir das Feld wirklich aktualisieren wollen, alle anderen Felder werden ignoriert.
MODIFY ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language UPDATE FROM VALUE #( ( %tky = ls_selected-%tky
targettext = ld_translated
%control-targettext = if_abap_behv=>mk-on ) )
FAILED DATA(ls_failed).
Zum Abschluss lesen wir noch einmal die Datensätze und geben den neuen Stand an das Ergebnis (RESULT) zurück, damit das Frontend sich aktualisieren kann. Für diese einfache Übernahme können wir eine FOR Schleife verwenden und übernehmen die Zuweisung und das Mapping mit einem Befehl.
READ ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_new).
result = VALUE #( FOR ls_new IN lt_new
( %tky = ls_new-%tky %param = ls_new ) ).
Die vollständige Methodenimplementierung findest du hier:
READ ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_selected).
LOOP AT lt_selected INTO DATA(ls_selected) WHERE TargetLanguage IS NOT INITIAL.
DATA(ld_translated) = NEW zcl_bs_demo_google_integration( )->translate_text(
id_target_language = to_lower( ls_selected-TargetLanguage )
id_text = CONV #( ls_selected-SourceText ) ).
MODIFY ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language UPDATE FROM VALUE #( ( %tky = ls_selected-%tky
targettext = ld_translated
%control-targettext = if_abap_behv=>mk-on ) )
FAILED DATA(ls_failed).
IF ls_failed-language IS INITIAL.
INSERT VALUE #( %tky = ls_selected-%tky ) INTO TABLE mapped-language.
ELSE.
INSERT VALUE #( %tky = ls_selected-%tky ) INTO TABLE failed-language.
INSERT VALUE #( %tky = ls_selected-%tky
%msg = new_message_with_text( severity = if_abap_behv_message=>severity-error
text = 'Error while updating the data' ) )
INTO TABLE reported-language.
ENDIF.
ENDLOOP.
READ ENTITIES OF ZBS_R_DMOLanguage IN LOCAL MODE
ENTITY Language ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_new).
result = VALUE #( FOR ls_new IN lt_new
( %tky = ls_new-%tky %param = ls_new ) ).
Komplettes Beispiel
Das komplette Beispiel findest du bei uns im GitHub Repository im Paket ZBS_DEMO_RAP_GOOGLE und alle Änderungen im passenden Commit des Repositories.
Fazit
In diesem Artikel haben wir uns im Detail die Schritte angeschaut, wie man die App verändert, anpasst und zum produktiven Stand bringt, sodass wir sie dem Anwender zur Verfügung stellen können. Grundsätzlich wäre die Anwendung noch runder mit Validierungen und Ermittlungen.