zondag 25 april 2010

GAE & Tapestry

Applicaties die moeten draaien in de Google App Engine kun je ontwikkelen in Eclipse en de door Google ontwikkelde plugin. Het maken en deployen van een applicatie is daarmee nog simpeler dan het genereren van een war-file voor Tomcat. Met een druk op de deploy knop wordt je applicatie online gezet.

Als eerste GAE projectje wilde ik een Hello World uit het Tapestry (een framework van Apache) project in GAE deployen. De vraag hierachter is hoe eenvoudig het is om een bestaand framework in de cloud te draaien.

Zoiets begint natuurlijk met even Googlen en de eerste hit is een tutorial hoe je tapestry 5.1 in de App Engine kunt draaien. Helaas werkte het niet (meer) zo simpel omdat een van de XML parsers die Tapestry gebruikt niet (meer) is goedgekeurd door Google. Er waren volgens andere blogs enkele hacks nodig om het aan de praat te krijgen, maar helaas leek ook dat niet het verschil te maken. (Het kan zijn dat ik iets over het hoofd zag.)

Op weer andere blogs las ik dat de nieuwe Tapestry 5.2 wel in GAE kon draaien. En inderdaad, de foutmelding zag er nu ineens anders uit. Het duurde nog even voordat ik ondekt had dat GAE case-sensitive is en dat de template files dus exact dezelfde naam moeten hebben als de bijbehorende classes (wat lokaal onder Windows niet het geval is). Daarmee rekening houdend, heb je het allemaal zo aan de praat en kunnen we vast stellen dat het gebruiken van een bestaand framework in de App Engine geen probleem is mits alle dependencies goedgekeurd zijn door Google.

Ik moet wel toegeven dat het mij enkele uren kostte om de simpele Hello World aan de praat te krijgen, daarom nog een keer de twee pointers:
  • Gebruik de snapshot van Tapestry 5.2;
  • GAE is case sensitive, noem daarom je class geen Index en de template index.tml.




http://www.atentia.net/2010/04/tapestry-on-gaej-java-lang-verifyerror-stack-size-too-large-solved/

Google App Engine

GWT genereert JavaScript dat wordt uitgevoerd in een browser. Zoals eerder geschreven, kun je daarmee vanuit de browser direct methodes op de server aanroepen. GWT zorgt voor de (un)marshalling. Deze in Java geschreven methodes zijn onderdeel van een Java Servlet. Zo'n Java Servlet heeft een container nodig waarbinnen hij kan draaien. Voorbeelden hiervan zijn Tomcat, Jetty, JBoss en Glassfish. Al deze containers zijn programma's welke op een computer kunt installeren en die bovenop de Java Virtual Machine een set features bieden welke handig kunnen zijn bij het ontwikkelen van Web Applicaties.

Wanneer je web applicatie populair wordt, kom je al snel in de problemen. De webserver/Java container kan maar een bepaald aantal verzoeken per tijdseenheid aan. Wanneer het aantal verzoeken blijft oplopen zullen er gebruikers zijn die niet te zien krijgen wat de bedoeling was. De oplossing voor dit probleem is het inzetten van meerdere servers. Hoeveel je er nodig hebt blijft een gok, dus worden er vaak veel te veel servers ingezet. Google had dit probleem ook en hun oplossing hiervoor hebben ze publiek toegankelijk gemaakt in de vorm van hun Google App Engine (GAE).

GAE is onder andere een op Jetty gebaseerde Java container en is onderdeel van de cloud. Wanneer een web applicatie die in deze App Engine draait veel gebruikt wordt, besluit de App Engine om meerdere instanties van de applicatie naast elkaar te gaan draaien, en de verzoeken te verdelen over deze instanties.

De opschaling problemen zijn hiermee verholpen maar helaas zijn er ook nadelen. Zo kun je geen gebruik maken van een SQL server in de cloud. Je bent gebonden aan BigTable (de database van Google) voor het opslaan van data. Daarnaast kun je ook niet zomaar alle jar-files in je project stoppen omdat niet alle Java functionaliteit aanwezig is en GAE. Jar-files moeten daarom worden goed gekeurd door Google.

Als je denkt dat opschalen een van de problemen is waarmee je te maken krijgt in de toekomst, en Java je favoriete ontwikkel taal is, is de Google App Engine zeker het overwegen waard. Als je daarnaast ook nog eens GWT gebruikt, lijkt het helemaal ideaal te zijn.

woensdag 14 april 2010

GWT Widget Custom Look&Feel

In GWT kun je snel dingen maken die leuk ogen, maar al snel komt het Ikea gevoel om de hoek kijken. Iemand komt je huiskamer binnen, ziet een Ikea-meubel staan, en moet even opmerken dat hij het meubel uit de catalogus herkent. Er is niks mis met het meubel, maar mensen weten meteen hoeveel je betaald hebt en waar ze er ook zo een kunnen halen. Zo gaat het ook een beetje met GWT. Neem bijvoorbeeld een DialogBox die mooi geanimeerd verschijnt. De mooie lichtblauw randjes en geronde hoekjes verraden het al snel: "Ah, je hebt GWT gebruikt." Wat kunnen we hieraan doen?

Google heeft dit helaas niet heel makkelijk gemaakt, maar er zijn twee dingen die je kunt doen. De randjes worden gemaakt door een drietal plaatjes. Zo zitten alle hoekjes in hetzelfde plaatje. Wanneer je je project compileert staan deze p[laatjes in /gwt/standard/images. Je kunt van deze plaatjes de kleur, hoek of schaduw aanpassen en opnieuw opslaan. De randen van een DialogBox zullen er dan iets anders uitzien, maar je bent in ieder geval van die blauwe kleur af.


Download ze hier.

De andere manier is wat ingrijpender. Hiervoor moet je de CSS-style voor de DialogBox overriden. Als je alle classes die de DialogBox nodig heeft opnieuw definieert, en vervolgens de StylePrimaryName van het dialogBox object aanpast, zal je DialogBox er ook anders uitzien. Hieronder staan de classen die gedefinieerd moeten worden. Voor het gemak heb ik er de uitleg uit de documentatie bijgezet en de oorspronkelijke waarde laten staan. De primaire naam is al wel aangepast.

.gwt-CustomDialogBox { 
  /*the outside of the dialog */ 
}

.gwt-CustomDialogBox .Caption { 
  /*the caption */
  background: #e3e8f3 url(images/hborder.png) repeat-x 0px -2003px;
  padding: 4px 4px 4px 8px;
  cursor: default;
  border-bottom: 1px solid #bbbbbb;
  border-top: 5px solid #89130e;
}

.gwt-CustomDialogBox .dialogContent { 
  /*the wrapepr around the content */
}

.gwt-CustomDialogBox .dialogTopLeft { 
  /*the top left cell */
  background: url(images/corner.png) no-repeat -13px 0px;
  -background: url(images/corner_ie6.png) no-repeat -13px 0px;
}

.gwt-CustomDialogBox .dialogTopLeftInner { 
  /*the inner element of the cell  */
  width: 5px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogTopCenter { 
  /*the top center cell, where the caption is located  */
}

.gwt-CustomDialogBox .dialogTopCenterInner { 
  /*the inner element of the cell */
}

.gwt-CustomDialogBox .dialogTopRight { 
  /*the top right cell  */
  background: url(images/corner.png) no-repeat -18px 0px;
  -background: url(images/corner_ie6.png) no-repeat -18px 0px;
}

.gwt-CustomDialogBox .dialogTopRightInner { 
  /*the inner element of the cell */
  width: 8px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogMiddleLeft { 
  /*the middle left cell */
  background: url(images/vborder.png) repeat-y;
}

.gwt-CustomDialogBox .dialogMiddleLeftInner { 
  /*the inner element of the cell */
}

.gwt-CustomDialogBox .dialogMiddleCenter { 
  /*the middle center cell, where the content is located  */
  padding: 3px;
  background: white;
}

.gwt-CustomDialogBox .dialogMiddleCenterInner { 
  /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogMiddleRight { 
  /*the middle right cell  */
  background: url(images/vborder.png) repeat-y -4px 0px;
  -background: url(images/vborder_ie6.png) repeat-y -4px 0px;
}

.gwt-CustomDialogBox .dialogMiddleRightInner { 
  /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogBottomLeft { 
  /*the bottom left cell */
  background: url(images/corner.png) no-repeat 0px -15px;
  -background: url(images/corner_ie6.png) no-repeat 0px -15px;
}

.gwt-CustomDialogBox .dialogBottomLeftInner { 
  /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogBottomCenter { 
  /*the bottom center cell */
  background: url(images/hborder.png) repeat-x 0px -4px;
  -background: url(images/hborder_ie6.png) repeat-x 0px -4px;
}

.gwt-CustomDialogBox .dialogBottomCenterInner { 
  /*the inner element of the cell  */
  width: 5px;
  height: 8px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogBottomRight { 
  /*the bottom right cell */
  background: url(images/corner.png) no-repeat -5px -15px;
  -background: url(images/corner_ie6.png) no-repeat -5px -15px;
}

.gwt-CustomDialogBox .dialogBottomRightInner { 
  /*the inner element of the cell  */
  width: 5px;
  height: 8px;
  zoom: 1;
}

In de CSS kun je de achtergronden van de hoekjes en randen van de DialogBox aanpassen. Het is het makkelijkst om per hoekje een apart plaatje te maken, maar je kunt ook zoveel mogelijk in één plaatje stoppen om het aantal downloads te beperken. Ik ben niet bepaald een Photoshop koning maar heb toch geprobeerd om een ander randje te maken. Zie hier het resultaat.


Hiervoor is de volgende CSS gebruikt:
.gwt-CustomDialogBox { 
 /*the outside of the dialog */ 
}

.gwt-CustomDialogBox .Caption { 
 /*the caption */
 background: url(img/t.png) repeat-x;
  padding: 4px 4px 4px 8px;
  cursor: default;
  
}

.gwt-CustomDialogBox .dialogContent { 
 /*the wrapepr around the content */
}

.gwt-CustomDialogBox .dialogTopLeft { 
 /*the top left cell */
 background: url(img/tl.png) no-repeat;
}

.gwt-CustomDialogBox .dialogTopLeftInner { 
 /*the inner element of the cell  */
 width: 12px;
 height: 12px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogTopCenter { 
 /*the top center cell, where the caption is located  */
}

.gwt-CustomDialogBox .dialogTopCenterInner { 
 /*the inner element of the cell */
 height: 12px;
}

.gwt-CustomDialogBox .dialogTopRight { 
 /*the top right cell  */
 background: url(img/tr.png) no-repeat;
}

.gwt-CustomDialogBox .dialogTopRightInner { 
 /*the inner element of the cell */
 width: 12px;
 height: 12px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogMiddleLeft { 
 /*the middle left cell */
 background: url(img/l.png) repeat-y;
}

.gwt-CustomDialogBox .dialogMiddleLeftInner { 
 /*the inner element of the cell */
}

.gwt-CustomDialogBox .dialogMiddleCenter { 
 /*the middle center cell, where the content is located  */
 padding: 1px;
  background: white;
}

.gwt-CustomDialogBox .dialogMiddleCenterInner { 
 /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogMiddleRight { 
 /*the middle right cell  */
 background: url(img/r.png) repeat-y;
}

.gwt-CustomDialogBox .dialogMiddleRightInner { 
 /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogBottomLeft { 
 /*the bottom left cell */
 background: url(img/bl.png) no-repeat;
}

.gwt-CustomDialogBox .dialogBottomLeftInner { 
 /*the inner element of the cell  */
}

.gwt-CustomDialogBox .dialogBottomCenter { 
 /*the bottom center cell */
 background: url(img/b.png) repeat-x;
}

.gwt-CustomDialogBox .dialogBottomCenterInner { 
 /*the inner element of the cell  */
 height: 12px;
  zoom: 1;
}

.gwt-CustomDialogBox .dialogBottomRight { 
 /*the bottom right cell */
 background: url(img/br.png) no-repeat;
}

.gwt-CustomDialogBox .dialogBottomRightInner { 
 /*the inner element of the cell  */
 width: 12px;
  height: 12px;
  zoom: 1;
}

Hier kun je de plaatjes downloaden.

GWT en Webservices

Een webservice is een service of programma welke beschikbaar is over een netwerk. Om gebruik te maken van de service, hoeft deze niet lokaal geinstalleerd te worden. De service kan over het netwerk aangeroepen worden. Het idee is vergelijkbaar met Remote Procedure Calls maar niet langer moet de service in dezelfde taal geschreven zijn als het programma dat gebruik wil maken van de service.

Een service wordt gedefinieerd in een op XML gebaseerde taal (WSDL). Dit WSDL bestand beschrijft wat de service doet, welke inputs nodig zijn, en welke outputs eruit komen. Aan de hand van deze WSDL kunnen de service- en clientstubs ontwikkeld worden. Veel software pakketten maken het mogelijk om een WSDL te genereren uit een reeds gebouwde service. Evengoed kan een clientstub gegenereerd worden uit deze WSDL-definitie. Voor het transport van de boodschappen worden ook XML berichten gebruikt (SOAP, XML-RPC of REST). Echter, nadat de service- en clientstub klaar zijn, heb je hier niks meer mee te maken. Je roept gewoon een methode aan in de clientstub en je krijgt, met een netwerk afhankelijke vertraging, een antwoord terug.

Het voordeel van webservices dat het platform onafhankelijk is. Betekent dat dat we een webservice ook kunnen aanroepen vanuit een website? Als de webservice draait op dezelfde server als waar de website draait, moet dat mogelijk zijn, maar wanneer deze op een andere server draait, komen we in aanraking met het "same origin policy". Het is in een browser om veiligheids redenen niet mogelijk om via JavaScript contact op te nemen met een vreemde server. Maar er is een omweg en GWT geeft ons wat we nodig hebben.

In GWT kunnen via RPC direct communiceren met Java code op de server. Voor onze server, geldt er geen "same origin policy", dus van daaruit kunnen gewoon gebruik maken van elke bereikbare webservice. Hieronder zal ik een voorbeeldje uitwerken. De webservice die ik zal gebruiken maakt het mogelijk om SQL achtige queries te doen op de databron achter de service. In die databron bevinden zich klantnamen. Wat we gaan maken is een textsuggestbox die suggesties levert die uit de webservice komen en afhankelijk zijn van de reeds ingetypte karakters.

De eerste stap bij het gebruiken van een webservice is het genereren van een clientstub. Er zijn een hoop verschillende tools die dit kunnen. Deze keer heb ik Axis (van Apache) gebruikt. Hierbij zit een scriptje genaamd WSDL2Java, wat precies doet wat je verwacht. Er wordt een class aangemaakt met daarin de methodes die de webservice levert. Ook worden er, indien nodig, allerlei complexe datatypen gemaakt.

De tweede stap is het aanmaken van een nieuw GWT project. Voeg hier de zojuist gegenereerde classes toe aan het classpath. Dit kan door alles in een jar-file in te pakken en deze in het buildpath van het project op te nemen, of door simpelweg alle genegereerde classes naar ons project te kopieren.

Definieer de volgende interface:
String[] getSuggestions(String start);
Maak hiervoor ook de asynchrome variant en implementeer de service op de webserver door gebruik te maken van de gegenereerde webservice clientstub. Ik ga ervanuit dat dat wel moet lukken.

Om onze suggestbox te voorzien van suggesties hebben we een zogenaamd Oracle nodig. Dit is een class, afgeleid van SuggestOracle, waarbij de requestSuggestions methode opnieuw gedefinieerd wordt. Dat ziet er als volgt uit:
private class XServerOracle extends SuggestOracle {

  @Override
  public void requestSuggestions(final Request request, final Callback callback) {
   testService.getSuggestions(request.getQuery(), new AsyncCallback() {
    @Override
    public void onSuccess(String[] result) {
     Collection suggestions = new ArrayList();
     for (final String row : result) {
      suggestions.add(new Suggestion() {
       @Override
       public String getDisplayString() {
        return row;
       }

       @Override
       public String getReplacementString() {
        return row;
       }
      });
     }
     Response resp = new Response(suggestions);

     callback.onSuggestionsReady(request, resp);
    }

    @Override
    public void onFailure(Throwable caught) {}
   });
  }
 }

Dit Oracle wordt vervolgens gebruikt bij het aanmaken van het SuggestBox object:
final SuggestBox box = new SuggestBox(new XServerOracle());

Klaar.

Een voorbeeld staat op:
http://waalwijk.yall.nl:8080/CustomerSearchWidget