Mit ‘Struts’ getaggte Artikel

Integration von Ext JS (AJAX) in eine bestehende Webanwendung

Montag, 02. November 2009

Das man mit JavaScript und AJAX nicht nur tolle, sondern auch wirklich nützliche Funktionalität in einer Webanwendung unterbringen kann, ist nicht von der Hand zu weisen. Eingabefelder, die schon beim Eintippen mögliche Treffer anzeigen, sind nur ein Beispiel und auf fast allen bekannten Webseiten zu finden.
Solche Funktionalität lässt sich auch in bestehenden Anwendungen nachrüsten, ohne das komplette Projekt umzukrempeln. Die folgenden Beispiele stammen aus der Erweiterung einer Struts 1 Anwendung, in die wir nachträglich Ext JS (Version 2.2) integriert hatten, um einige der Widgets von Ext JS zu nutzen.

1. Ext JS zum Projekt hinzufügen
2. Einbinden von Ext JS in die JSP’s
3. AJAX Request mit Ext JS (Beispiel)
4. Response (HTML oder JSON)

Anmerkung: Expression Language (EL) muss vom Server unterstützt werden.

1. Ext JS zum Projekt hinzufügen

Das Archiv, das man von der Ext JS Seite herunterladen kann, enthält neben den eigentlichen Bibliotheken auch zahlreiche Beispiele und Sourcen, die von der Anwendung nicht benötigt werden. Folgende Dateien* und Verzeichnisse* kopiert man in sein Projekt z.B. in das Unterverzeichnis WebContent/js/ext:
- ext-all.js
- ext-core.js
- /adapter
- /build
- /resources

* Ext JS Version 2.2 oder 2.3 ohne Debug

Man kann auch die oben erwähnten Dateien und Verzeichnisse in ein ZIP packen, z.B. ext-2.2.zip und dieses beim Build der Anwendung durch Ant enstspr. auspacken und zum WAR hinzufügen lassen. Damit ist einigermaßen sichergestellt, dass die Ext-Dateien nicht editiert werden und zudem hat man nur eine Datei auf dem SVN bzw. CVS liegen (erleichtert auch den Wechsel auf eine andere Ext-Version). Das ist aber absolut optional.

2. Einbinden von Ext JS in die JSP’s

Um Ext verfügbar zu machen und den Standardadapter zu nutzen sind folgende Zeilen im HTML-Head nötig:

<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/ext/ext-all.js"></script>
<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/ext/adapter/ext/ext-base.js"></script>

Will man einige der Widgets von Ext nutzen, ist zusätzlich das CSS von Ext einzubinden:

<link rel="stylesheet" type="text/css" 
href="${pageContext.request.contextPath}/js/ext/resources/css/ext-all.css" />

und ggf. ein Theme auszuwählen, wenn man nicht das Standard-Blau verwenden möchte:

<link rel="stylesheet" type="text/css" id="theme" 
href="${pageContext.request.contextPath}/js/ext/resources/css/xtheme-gray.css" />

Eine Besonderheit: Ich weiß nicht, ob die Ext Entwickler das mit Absicht gemacht haben und welchen Zweck es haben soll, aber Ext “funkt” nach Hause. An einigen Stellen wird ein 1×1 Pixel Image von der Ext-Seite abgerufen. Will man das umgehen und stattdessen das Image lokal laden, sollte man diese Zeilen im Header unterbringen:

<script type="text/javascript">
// url for empty image otherwise http://extjs.com will be called
Ext.BLANK_IMAGE_URL = '${pageContext.request.contextPath}/js/ext/resources/images/default/s.gif';
</script>

3. AJAX Request mit Ext JS (Beispiel)

Nun werden wir einen Ajax Request ausführen um z.B. den Wert eines Formularfelds an den Server zu übermitteln, ohne die komplette Seite neu laden zu müssen (also kein Form.submit()).
Die Funktion würde wie folgt aussehen:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      // in diesem Fall soll nichts gemacht werden	
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Serverseitig rufen wir hiermit eine entspr. gemappte StrutsAction auf.
Auszug aus der struts-config.xml:

<action path="/sendValue"
  type="de.baderundjene.example.ValueChangeAction">
</action>

Ein Form kann man sich sogar sparen und den Parameter direkt auslesen:

public final class ValueChangeAction extends Action {
  @Override
  public final ActionForward execute(....) {
    final String newValue = request.getParameter("newValue");
    // irgendwas mit dem newValue machen
 
    return null;
  }
}

Am Ende geben wir null zurück, es soll ja kein ActionForward erfolgen und der Response soll leer sein.

4. Response (HTML oder JSON)

Möchten wir aber einen Wert, Werte oder ein HTML-Fragment zurückgeben, muss dieses als String in den Response geschrieben werden.
Um Daten an den Client (Browser) zu schicken, die dort per JavaScript verarbeitet werden sollen um z.B. die Werte einer Select-Box zu verändern, eignet sich JSON (JavaScript Object Notation) besonders gut. Details zu JSON findet man ganz leicht im Internet. Bevor wir also in unserer Action null zurückgeben, schreiben wir unseren JSON-String in den Response:

// ein Attribut data mit dem Wert Test
final String json = "({data:'Test'})";
// HttpServletResponse response;
// hier können wir auch das Encoding auf z.B. UTF-8 stellen
response.setContentType("text/json; charset=UTF-8");
final PrintWriter out = response.getWriter();
out.println(json);
out.flush();

Im JavaScript erweitern wir unsere success Methode:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      var responseObj = Ext.decode(result.responseText);
      Ext.Msg.alert('Erfolg', responseObj.data);
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Möchte man ein HTML-Fragment zurückgeben, um z.B. einen Teil der Seite neu zu laden, kann man in der Action einen ActionForward auf eine entspr. JSP durchführen. Diese JSP enthält dann eben keine ganze HTML-Seite sondern nur den Teil, den wir neu laden möchten, z.B. eine Tabelle mit den Kundendaten.
D.h. struts-config.xml anpassen:

<action path="/sendValue"
  type="de.baderundjene.example.ValueChangeAction">
  <forward name="success" path="inc/customerTable.jsp"/>
</action>

und statt null den ActionForward mapping.findForward(”success”); zurückgeben.
Unsere angepasste JavaScript Methode:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      // <div id="customer-data">&nbsp;</div>
      var customerDiv = Ext.get('customer-data');
      customerDiv.update(response.responseText, true);
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Fazit

Ein Projekt kann sehr leicht mit Ajax erweitert werden ohne bestehende Funktionalität anzufassen. Welches MVC-Framework hier zum Einsatz kommt, spielt eigentlich keine Rolle. Modernere Frameworks bieten allerdings teilweise out-of-the-box Unterstützung für Ajax und JSON an.
Bei unserem Projekt haben wir neue Erweiterungen direkt mit Ext JS implementiert, möglichst alle eigenen oder sonstiges JavaScript durch Ext Funktionen ersetzt und bauen jetzt bei Bedarf und wenn es sinnvoll ist, ältere Teile der Anwendung auf Ext um. Z.B. verwenden wir das Tree Widget inkl. Drag and Drop und Kontextmenü, welches sehr mächtig ist und einen sehr stabilen Eindruck macht.
Zu wünschen wäre jetzt nur noch eine bessere Unterstützung durch eclipse, aber das kommt vielleicht auch noch.
Falls es sich ergibt, werde ich auch noch näher auf interessante Widgets von Ext JS eingehen.

Viel Spaß und Erfolg beim Ausprobieren und Anwenden.


Christof Aenderl


Eigene (Property)MessageResources in Struts

Mittwoch, 28. Mai 2008

Für die Internationalisierung von Web-Anwendungen bietet das Java-Webframework Struts bereits eingebaute Mechanismen und vorgefertigte Lösungen. Textbausteine werden dabei immer über einen eindeutigen Key referenziert. Die jeweilige sprachabhängige Ausprägung bestimmt ein Locale Objekt.

Standardmäßig verwendet Struts PropertyMessageResources. Dabei werden die Texte in altbekannten Properties-Dateien in Form von Key-Value Paaren gespeichert. Pro gewünschter Sprache wird dann eine solche Datei benötigt.

Möchte man nun aber andere Mechanismen nutzen (etwa in einer Datenbank gespeicherte Texte) oder Legacy-Anwendungen mit proprietären Internationaliserungs-Konzepten auf Struts umstellen, ist ein wenig mehr Aufwand nötig. Um etwa ein Alt-Anwendung umzustellen, die zwar Properties-Dateien in Struts Manier nutzt, jedoch über ein eigenes Konstrukt Konstanten innerhalb der Textbausteine platziert, könnte man neue Tags bauen oder alle Properties-Dateien anfassen. Eleganter ist es, seine eigene MessageResources zu implementieren:

(die hier vorgestellte Lösung bezieht sich auf das Struts-Framework Version 1.2.7)

Da die Altanwendung ebenfalls auf Properties-Dateien setzte, bietet sich eine Erweiterung der Struts PropertyMessageResource an:

public class MyMessageResources extends PropertyMessageResources {
...
     @Override
     public String getMessage(Locale locale, String key) {
       String message = super.getMessage(locale, key);
       while(containsConstant(message)) {
           message = substituteConstant(message, key);
       }
       return message;
     }
...
}

Um das Verhalten anzupassen, muss nun lediglich die Methode getMessage der PropertyMessageResources Klasse überschrieben werden. Diese kann die ursprüngliche Message über die überschriebene Methode erfragen und entsprechend die Konstanten ersetzen. So weit so gut, aber wie schafft man es nun, dass seine eigene MessageResources in der Applikation auch benutzt werden? Dafür benötigt man zuallererst eine MessageResourcesFactory, die eine Instanz einer konkreten MessageResources liefert. Im hier geschilderten Fall kann dafür die vorhandene Factory für die PropertyMessageResources erweitert werden:

public class MyPropertyMessageResourcesFactory extends
      PropertyMessageResourcesFactory {
 
     @Override
     public MessageResources createResources(String config) {
	MyPropertyMessageResources messageResources =
            new MyPropertyMessageResources(this, config,
                this.returnNull);
        return messageResources;
     }
}

Als letzten Schritt muss man Struts die Factory nun in der struts-config.xml bekannt machen:

    <message-resources 
        parameter="languageBundle" null="false"
 	factory="MyPropertyMessageResourcesFactory"/>

Normalerweise wird lediglich der Name der Properties-Dateien (ohne Locale- und Dateisuffix) angegeben (parameter). In diesem Fall würde dann standardmäßig die PropertyMessageResourcesFactory Verwendung finden. Um nun seine eigene Factory zu benutzen, wird die Factory-Klasse (voll qualifizierter Klassenname) einfach über das factory Attribut bekannt gemacht. Und schon ist es vollbracht! Eigentlich sehr leicht, wenn man es denn weiß… :-)


Christian Schätzlein