Saturday, September 27, 2008

Masking the URL bar on an iphone web application

With the firmware 2.1, it’s possible to mask the URL bar with a meta tag. Previous this meta tag instruction, the masking was done with javascript code.

 

Adding <meta name="apple-mobile-web-app-capable" content="yes" /> instruction in the HTML code will result in masking the URL bar.

Take attention that this instruction is only valid when your webapp is added as the home screen.

Thursday, September 04, 2008

Using the apt ant task

I'm using Jdeveloper and there is not support for the apt task in this IDE. Ant 1.7.0 has a built in support task for Apt.
To use it, add the apt tag in your build file as followed.

Saturday, July 26, 2008

GWT-Ext: Wrapping a Google Map component

The aim of this tutorial is to present how to wrap a Google Map component with GWT-Ext.

GWT-Ext proposes a lot of Google Map wrappers. The tutorial proposes to demonstrate how to wrap the “Direction” and “Map” components. Thanks this component it’s now easy to reuse a Direction in a web page.

Direction widget features:

directions

  • Map and direction in the same component
  • Map and directions size are automaticaly adjusted depending component size
  • Optional form to define origin and destination
  • Horizontal or vertical layout

 

Main steps to create the widget:

Overloading the Panel component
Creating the “<Div>” elements used by the Google Map API
Those 2 elements are used by the Google API (next step) to display the map and the directions components.
        String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);



Initializing the Google Map API
GMap2 and GDirections javascript objects are initialized. $wnd is used by GWT API to access javascript objects.
    private native JavaScriptObject initializeMap() /*-{
var map = new $wnd.GMap2($doc.getElementById('widget_map_canvas'));
map.addControl(new $wnd.GSmallMapControl());
map.addControl(new $wnd.GMapTypeControl());
return map;
}-*/;

private native JavaScriptObject initializeDirections(Directions thisModule) /*-{
var map = this.@com.benevolarc.gwt.client.Directions::_map;
var gdir = new $wnd.GDirections(map, $doc.getElementById('widget_directions'));
$wnd.GEvent.addListener(gdir, "load", function(response) {
if (response && response.getStatus().code == 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsOK()();
}
});



Creating the layout (horizontal or vertical) to store the Map and the Direction components
The layout allows to display the map and the directions with 2 options (left-rigth or top-down)
Muliple GWT-Ext layouts are used for that purpose:

  1. FitLayout is used a base class for layouts that contain a single item that automatically expands to fill the layout's container

  2. Panels with ColumLayoutData are used for the Horizontal mode

  3. AnchorLayoutData with PanelListenerAdapter is used for the Horizontal mode


The difficulty was to display an horizontal panel with a height expressed in percentage instead of pixel. The solution was to use a PanelListenerAdapter.
    private void init() {
setLayout(new FitLayout());
setBorder(false);
String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);

//_panel.add(new HTMLPanel("
"));

if (_displayMode == DisplayMode.HORIZONTAL) {
Log.debug("Horizontal mode");
Panel columnLayoutPanel = new Panel();
columnLayoutPanel.setBorder(false);
columnLayoutPanel.setLayout(new ColumnLayout());

col1Panel = new Panel();
col1Panel.setBorder(false);
col1Panel.setLayout(new FitLayout());
Panel panelMap = new Panel();
panelMap.setBorder(false);
_mapHTMLPanel.setMargins(10, 10, 10, 10);
panelMap.setLayout(new AnchorLayout());
if (_displayForm) {
panelMap.add(createFromToForm());
}
panelMap.add(_mapHTMLPanel, new AnchorLayoutData("100% 100%"));
col1Panel.add(panelMap);
col2Panel = new Panel();
col2Panel.setBorder(false);
col2Panel.setLayout(new FitLayout());
col2Panel.add(_directionsHTMLPanel);

columnLayoutPanel.add(col1Panel, new ColumnLayoutData(.50));
columnLayoutPanel.add(col2Panel, new ColumnLayoutData(.50));
columnLayoutPanel.addListener(new PanelListenerAdapter() {
public void onResize(BoxComponent component, int adjWidth, int adjHeight, int rawWidth, int rawHeight) {
col1Panel.setHeight(rawHeight);
col2Panel.setHeight(rawHeight);
}
});


add(columnLayoutPanel);

} else {
Log.debug("Vertical mode");
Panel panel = new Panel();
panel.setLayout(new AnchorLayout());
if (_displayForm) {
FormPanel fPanel = createFromToForm();
panel.add(fPanel);
}
panel.add(_mapHTMLPanel, new AnchorLayoutData("100% 50%"));
_mapHTMLPanel.collapse();
panel.add(_directionsHTMLPanel, new AnchorLayoutData("100% 50%"));
add(panel);
}
}



Invoking the directions
When the user clicks on the "get directions" button a call to the Google API has to be done. A listener is attached on the button and a call to the JSNI method is done
        
goButton.addListener(new ButtonListenerAdapter() {
public void onClick(Button button, EventObject e) {
lauchDirections();
}
});

launchDirections checks if origin and destination are correctly filled and call the setDirections native method.
    private void lauchDirections() {
Log.debug("Getting direction" + _map);
if (_toTextField.getText().length() > 0 && _fromTextField.getText().length() > 0) {
setDirections(_fromTextField.getValueAsString(), _toTextField.getValueAsString(), _locale);
clearErrorMessage();
} else {
Log.warn("From and to must be filled");
setErrorMessage("From and to must be filled");
}
}

The setDirections method calls the Google API method gdir.load. In case of success, the Map and the directions components are updated. In case of error, a message is displayed to the user.
  public native void setDirections(String fromAddress, String toAddress, String locale) /*-{
var gdir = this.@com.benevolarc.gwt.client.Directions::_gdir;
gdir.clear();
gdir.load("from: " + fromAddress + " to: " + toAddress,
{ "locale": locale });
}-*/;


Using the Directions component

Using the component is easy

  1. Displaying the Directions and Map component with origin and destination in the constructor (horizontal layout)
    Default component size is 600px*600px

    directions = new Directions("paris","bordeaux", Directions.DisplayMode.HORIZONTAL);


  2. Displaying the Form allowing the user to enter origin and destination (horizontal layout)

    //directions with Horizontal layout
    directions = new Directions(true, Directions.DisplayMode.HORIZONTAL);
    directions.setWidth("1000px");
    directions.setHeight("800px");
    directions.setBorder(true);



  3. Displaying the Form allowing the user to enter origin and destination vertical layout)

    //directions with Horizontal layout
    directions = new Directions(true, Directions.DisplayMode.VERTICAL);
    directions.setBorder(true);




Conclusion

The tutorial demonstrates how to wrap a Google component. Of course this component is not perfect and a lot of improvement needs to be done.

  1. Improve the look and feel

  2. Support to Google street

  3. Internationalization support

  4. Printing capabilities

  5. And more...


Feel free to reuse it and please don't hesitate to give your opinion. If you find this component usefull, the source can be use for GWT-EXT extenstion...
Thanks for GWT-Ext forum for the support regarding layout management.


Complete Code


 
package com.benevolarc.gwt.client;

import com.allen_sauer.gwt.log.client.Log;

import com.google.gwt.core.client.JavaScriptObject;

import com.google.gwt.user.client.ui.HTML;

import com.gwtext.client.core.EventObject;
import com.gwtext.client.widgets.BoxComponent;
import com.gwtext.client.widgets.Button;
import com.gwtext.client.widgets.HTMLPanel;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.event.ButtonListenerAdapter;
import com.gwtext.client.widgets.event.PanelListenerAdapter;
import com.gwtext.client.widgets.form.Field;
import com.gwtext.client.widgets.form.FormPanel;
import com.gwtext.client.widgets.form.MultiFieldPanel;
import com.gwtext.client.widgets.form.TextField;
import com.gwtext.client.widgets.form.event.FieldListenerAdapter;
import com.gwtext.client.widgets.layout.AnchorLayout;
import com.gwtext.client.widgets.layout.AnchorLayoutData;
import com.gwtext.client.widgets.layout.ColumnLayout;
import com.gwtext.client.widgets.layout.ColumnLayoutData;
import com.gwtext.client.widgets.layout.FitLayout;


public class Directions extends Panel{
//private Panel _panel;
private TextField _fromTextField;
private TextField _toTextField;
private HTMLPanel _mapHTMLPanel;
private HTMLPanel _directionsHTMLPanel;
private JavaScriptObject _map;
private JavaScriptObject _gdir;
public enum DisplayMode {HORIZONTAL, VERTICAL};
private DisplayMode _displayMode = DisplayMode.VERTICAL;
private boolean _displayForm = true;
private Panel col1Panel = null;
private Panel col2Panel = null;
private HTML _errorMessage = new HTML();
private String _from = null;
private String _to = null;
private String _locale = "EN";

public Directions(boolean displayForm) {
_displayForm = displayForm;
init();
}

public Directions(boolean displayForm, DisplayMode displayMode) {
_displayForm = displayForm;
_displayMode = displayMode;
init();
}

public Directions(String from, String to, DisplayMode displayMode) {
_displayMode = displayMode;
_displayForm = false;
_from = from;
_to = to;
init();
}

private FormPanel createFromToForm() {
_fromTextField = new TextField("From", "from", 250);
_toTextField = new TextField("To", "to", 250);
Button goButton = new Button("Get directions");

MultiFieldPanel directionsFormPanel = new MultiFieldPanel();

directionsFormPanel.setBorder(false);
directionsFormPanel.setPaddings(10, 10, 10, 10);
directionsFormPanel.addToRow(_fromTextField, 300);
directionsFormPanel.addToRow(_toTextField, 300);
directionsFormPanel.addToRow(goButton, 100);

FormPanel fPanel = new FormPanel();
fPanel.setBorder(false);
fPanel.setLabelWidth(30);
fPanel.add(_errorMessage);
fPanel.add(directionsFormPanel);
_fromTextField.addListener(new FieldListenerAdapter() {
public void onSpecialKey(Field field, EventObject e) {
if (e.getKey() == EventObject.ENTER) {
lauchDirections();
}
}
});
_toTextField.addListener(new FieldListenerAdapter() {
public void onSpecialKey(Field field, EventObject e) {
if (e.getKey() == EventObject.ENTER) {
lauchDirections();
}
}
});

goButton.addListener(new ButtonListenerAdapter() {
public void onClick(Button button, EventObject e) {
lauchDirections();
}
});
return fPanel;
}

private void setErrorMessage(String message) {
_errorMessage.setHTML("
Error: "+ message + "
");
}

private void clearErrorMessage() {
_errorMessage.setHTML("
");
}

private void lauchDirections() {
Log.debug("Getting direction" + _map);
if (_toTextField.getText().length() > 0 && _fromTextField.getText().length() > 0) {
setDirections(_fromTextField.getValueAsString(), _toTextField.getValueAsString(), _locale);
clearErrorMessage();
} else {
Log.warn("From and to must be filled");
setErrorMessage("From and to must be filled");
}
}

private void init() {
Log.debug("Setting default width and heigth");
setWidth(600);
setHeight(600);
setLayout(new FitLayout());
setBorder(false);
String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);

//_panel.add(new HTMLPanel("
"));

if (_displayMode == DisplayMode.HORIZONTAL) {
Log.debug("Horizontal mode");
Panel columnLayoutPanel = new Panel();
columnLayoutPanel.setBorder(false);
columnLayoutPanel.setLayout(new ColumnLayout());

col1Panel = new Panel();
col1Panel.setBorder(false);
col1Panel.setLayout(new FitLayout());
Panel panelMap = new Panel();
panelMap.setBorder(false);
_mapHTMLPanel.setMargins(10, 10, 10, 10);
panelMap.setLayout(new AnchorLayout());
if (_displayForm) {
panelMap.add(createFromToForm());
}
panelMap.add(_mapHTMLPanel, new AnchorLayoutData("100% 100%"));
col1Panel.add(panelMap);
col2Panel = new Panel();
col2Panel.setBorder(false);
col2Panel.setLayout(new FitLayout());
col2Panel.add(_directionsHTMLPanel);

columnLayoutPanel.add(col1Panel, new ColumnLayoutData(.50));
columnLayoutPanel.add(col2Panel, new ColumnLayoutData(.50));
columnLayoutPanel.addListener(new PanelListenerAdapter() {
public void onResize(BoxComponent component, int adjWidth, int adjHeight, int rawWidth, int rawHeight) {
col1Panel.setHeight(rawHeight);
col2Panel.setHeight(rawHeight);
}
});


add(columnLayoutPanel);

} else {
Log.debug("Vertical mode");
Panel panel = new Panel();
panel.setLayout(new AnchorLayout());
if (_displayForm) {
FormPanel fPanel = createFromToForm();
panel.add(fPanel);
}
panel.add(_mapHTMLPanel, new AnchorLayoutData("100% 50%"));
_mapHTMLPanel.collapse();
panel.add(_directionsHTMLPanel, new AnchorLayoutData("100% 50%"));
add(panel);
}
}

protected void onAttach() {
Log.debug("Component attached");
_map = initializeMap();
_gdir = initializeDirections(this);
if (_from != null && _to != null) {
setDirections(_from, _to, _locale);
} else {
_mapHTMLPanel.collapse();
}
super.onAttach();
}

private native JavaScriptObject initializeMap() /*-{
var map = new $wnd.GMap2($doc.getElementById('widget_map_canvas'));
map.addControl(new $wnd.GSmallMapControl());
map.addControl(new $wnd.GMapTypeControl());
return map;
}-*/;

private native JavaScriptObject initializeDirections(Directions thisModule) /*-{
var map = this.@com.benevolarc.gwt.client.Directions::_map;
var gdir = new $wnd.GDirections(map, $doc.getElementById('widget_directions'));
$wnd.GEvent.addListener(gdir, "load", function(response) {
if (response && response.getStatus().code == 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsOK()();
}
});

$wnd.GEvent.addListener(gdir, "error", function(response) {
if (!response || response.getStatus().code != 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsError(I)(response.getStatus().code);
}
});
return gdir;
}-*/;

public void directionsError(int error) {
Log.debug("Error");
String errorMessage = "Unknown error";
switch (error) {
case 601: errorMessage = "Missing query";break;
case 602: errorMessage = "Unknown address";break;
case 603: errorMessage = "Unavailable address";break;
case 604: errorMessage = "Unknown direction";break;
default: errorMessage ="Unknown error";
}
setErrorMessage(errorMessage);
_mapHTMLPanel.collapse();
_directionsHTMLPanel.collapse();
}

public void directionsOK() {
Log.debug("Directions OK");
_mapHTMLPanel.expand(true);
_directionsHTMLPanel.expand(true);
}


public native void setDirections(String fromAddress, String toAddress, String locale) /*-{
var gdir = this.@com.benevolarc.gwt.client.Directions::_gdir;
gdir.clear();
gdir.load("from: " + fromAddress + " to: " + toAddress,
{ "locale": locale });
}-*/;

public void setLocale(String locale) {
this._locale = locale;
}

public String getLocale() {
return _locale;
}



}

Sunday, July 20, 2008

Google App Engine: Queries as an SQL “LIKE”

Introduction:

When i have developed my first GAE application, i wanted to use a request with a “LIKE” to find all tuples beginning with a prefix.
GQLQuery does not allow this kind of SQL request (SELECT * FROM user where name LIKE ‘SMIT%’).
The solution is to use this kind of request:


query = self.request.str_POST['query']
queryEnd = query+"\xEF\xBF\xBD".decode('utf-8')
res=db.GqlQuery('SELECT * FROM User WHERE name>=:1 AND name<=:2 ORDER BY nom DESC', query, queryEnd)


I was obliged to modify the sample code provided by the documentation (here)
queryEnd = query+"\xEF\xBF\xBD".decode('utf-8') was added.

Thanks a lot to José Oliver Segura for his support.

Information can be found here

Thursday, July 17, 2008

Inserting code snippet in Blogger

It was really difficult for me to find a solution to insert java code snippets in my Blogger !!
I found a solution using the syntaxhighlighter JavaScript plugin.
The steps used to insert code snippet:
  • Download syntaxhighlighter from syntaxhighlighter
  • Unzip the file and place the Scripts and Styles directories in a URL (location in my sample)
  • Modify your blogger template adding those lines just after the </div></div> <!-- end outer-wrapper –>
  • To publish a post with code snippet, insert your code inside the <pre> or <textarea> tags

<textarea name="code" class="c#" cols="60" rows="10">

... some code here ...

</textarea>

Tuesday, July 15, 2008

Rounded corners panel with GWT

Native GWT solution

GWT provides a native solution to build panels with rounded corners. This solution is done with a DecoratorPanel component.

The rounded corners are build thanks a .css file customization where images needs to be provided in the css file.

html>body .gwt-DecoratorPanel .topLeft {
  background: url(images/corner.png) no-repeat 0px 0px;
}

The drawback of this solution is that images need to be provided when a new rounded panel has to be created with a new color.

Bouwkamp solution

An other solution without providing any images for corner definition is available thanks http://code.google.com/p/com-bouwkamp-gwt/. By this way creating a new rounded corner panel with different colors is a very basic task.

Steps to create a new rounded panel:

  • Download the jar file and reference this file in the classpath
  • Modify the <appli>.gwt.xml and add :

<inherits name='com.bouwkamp.gwt.user.User' />

  • Create your java file:

    By default the height of the corners is 2px. It is possible to set a different height at construction time. The height can be a value between and including 1 and 9. This value doesn't correspond exactly with the height, e.g. 9 is 12px height.

     // all 4 corners are rounded and height index 5
    RoundedPanel rp = new RoundedPanel(yourWidget, ALL, 5);



    In the previous sample yourWidget can be any component like a VerticalPanel



    You can even define the color of the corner programaticaly:




    // all 4 corners are rounded.
    RoundedPanel rp = new RoundedPanel(yourWidget);
    rp.setCornerColor("red");




  • Customize the .css application file for rounder corner customization


  • Default the css style name of the rounded corner divs is cbg-RP. Use it to set the colors of the corner. For example:




     .cbg-RP { background-color:#c3d9ff; }



    Conclusion



    The solution provided by the Bouwkamp library is very easy to use. Some improvement can be done like providing a rounded corner support and shadow borders. It seems javascript libary is available for this feature see:



    http://www.ruzee.com/blog/shadedborder/






 

Saturday, July 12, 2008

GWT-Ext, Remote ComboBox tutorial

Designing a local comboBox is a basic documented task. For the remote mode (where data to be suggested are returned from a server) the documentation is not so clear.

In this post, a basic review of the LOCAL mode is done and in a second part, the REMOTE mode is developped.

Local comboBox mode:

Basic steps:

  1. Store initialization
  2. Loading the store
  3. Creating the combobox with LOCAL mode
  4. Linking the combobox with the store

Sample code:

        final Store store = 
new SimpleStore(new String[] { "civilite", "desc", },
new String[][] { new String[] { "Mr", "Mr" },
new String[] { "Mlle", "Mlle" },
new String[] { "Mde", "Mde" } });
store.load();

final ComboBox cb = new ComboBox();
cb.setForceSelection(true);
cb.setMinChars(1);
cb.setFieldLabel("Civilité");
cb.setStore(store);
cb.setAllowBlank(false);
cb.setDisplayField("civilite");
cb.setMode(ComboBox.LOCAL);
cb.setTriggerAction(ComboBox.ALL);
cb.setEmptyText("Enter civilité");
cb.setLoadingText("Recherche...");
cb.setTypeAhead(true);
cb.setEditable(false);
cb.setSelectOnFocus(true);
cb.setWidth(230);

cb.setHideTrigger(false);
add(cb);


Remote comboBox mode:


In that case, the store needs to be constructed dynamicaly from the server.

Each time the user tapes a new key in the combobox, a request has to be sent to the server to update the suggest box. The current combobox value has to be sent as a parameter to the server.

The combobox REMOTE mode uses a POST query to the server with the query parameter filled with the current combobox value.

The server side (any servlet or CGI) needs to analyze the POST request, retrieve the query parameter and return an answer to be displayed in the combobox (in the sample code, JSON is used). Some parameters can be set for autocomplete configuration:

setMinChar: The minimum number of characters the user must type before autocomplete activate (default is 4)

setQueryDelay: The length of time in milliseconds to delay between the start of typing and sending the query to filter the dropdown list (defaults to 500 ms)

Basic steps:

  1. Store creation and loading with a HttpProxy and a JsonReader

  2. Combobox creaction with REMOTE mode

  3. Linking the combobox with the store

  4. Creating the server side part (servlet or CGI) by analyzing the query parameter on the POST request.



Sample code:



         HttpProxy dataProxy = new HttpProxy("/rpc/adherents");
final String resultTpl = "

{nom} {prenom}

{email}
";
RecordDef adherentsRecordDef =
new RecordDef(new FieldDef[] { new StringFieldDef("id"),
new StringFieldDef("nom"),
new StringFieldDef("prenom"),
new StringFieldDef("telephoneFixe"),
new StringFieldDef("telephoneMobile"),
new StringFieldDef("email"),
new StringFieldDef("adresse"),
new StringFieldDef("codePostal"),
new StringFieldDef("ville") });
JsonReader reader = new JsonReader(adherentsRecordDef);
reader.setRoot("adherents");
Store store = new Store(dataProxy, reader);
store.load();
ComboBox cb = new ComboBox();
cb.setStore(store);
cb.setDisplayField("nom");
cb.setId("nom");
cb.setTypeAhead(false);
cb.setLoadingText("Searching...");
cb.setFieldLabel("Nom");
cb.setWidth(230);
cb.setTpl(resultTpl);
cb.setPageSize(10);
cb.setHideTrigger(true);
cb.setMode(ComboBox.REMOTE);
cb.setTitle("Users");
//cb.setHideLabel(true);
cb.setItemSelector("div.search-item");

Sunday, June 29, 2008

Google Calendar API: managing main and secondary calendars

Adding an event in a Google calendar is pretty easy. For me, the only touchy think is to select the correct calendarID.

  • To add an event in the main calendar (the default calendar created by Google when the account is created) the url is: /calendar/feeds/default/private/full
  • To add an event in a secondary calendar the url is: /calendar/feeds/<calendarID>/private/full

To find the calendarID:

1) Go to Google Calendar.
2) Click on "Manage calendar
3) Select the tab Calendars
4) Click on the name of the secondary calendar
5) You will see an entry that says "Calendar address"
6) Search (CalendarID:...@group.calendar.google.com)

Don't forget to replace the @ with %40 when used in you application.

Sunday, June 22, 2008

Google App Engine: basic tips

  • To start the local python server in debug mode:

python dev_appserver.py –debug

  • To reset the datastore:

python dev_appserver.py --clear_datastore

  • To use Google App Engine behind a proxy

With Windows, 2 system variables need to be set:

http_proxy =http://<login>:<password>@<proxy_address>:<proxy_port>

https_proxy = "http://<login>:<password>@<proxy_address>:<proxy_port>

  • To access the local datastore to view the application data:

http://localhost:8080/_ah/admin

Sunday, May 04, 2008

EJB3 and JUnit

For EJB3 testing, an interesting solution based on JBoss embedded package. Of course other alternative are available (Cactus, EJB3Unit...).

Thanks to this solution, it is now possible to launch an embedded JBoss container in standalone (without the need to install and configure a full JBoss container).

This solution is very interesting for testing purpose because there is no need to deploy and un-deploy each time on the server.

This JBoss package includes all mandatory libraries used to validated an EJB3 solution:

  • 1 EJB3 container
  • The EJB3 Hibernate implementation
  • 1 embedded java database: HSQLDB
  • JUnit (3) for testing purpose

For this tutorial, the embedded-jboss-beta3 is used and can be downloaded here

All the tutorial sources is available here.

Tutorial objective: Testing an EJB3 solution with JUnit based on

  • 1 entity bean (Person)
  • 1 session bean used for CRUD operation (Create, Remove, Update, Display)
  • 1 JUnit test case used to validate session bean operation

Step 1: Download the embedded JBoss library

Step 2: Create a startup project

Create a project with your favorite IDE and import the JBoss embedded library (from the library and from the optional-lib directories). Do not forget to add all the .jar files in the classpath. Embedded configuration directory needs to be in the classpath also (/embedded-jboss/bootstrap).

Step 3: Create your Entity bean

For this tutorial, the Person class is created

Step 4: Create your session bean

For this tutorial, the PersonService session bean (interface and local implementation)

Step 5: Create your JUnit test case

It's the most interesting part of the tutorial.

To create and deploy JUnit tests easily, JBoss provides an extension of the JUnit TestCase class, the BaseTestCase. This class needs to package the application hosting the testes. A dynamical jar builder is used to create the package.

Step 6: Build your applications and tests with ant

Ant 1.7 is used for this tutorial.

Step 7: Run the tests

The tests ant task is used. This task will execute all tests available in the test directory.

An other ant task tests-report-html can be used to generate the report in html format.

Step 8 (optional): Running multiple tests

To avoid deploying and starting the JBoss container for each test case, it's possible to group all test cases in a suite by extending the JUnit TestSuite class.

For the tutorial, the AllTests class has been used and 2 test cases has been added in the suite. The all-tests task is used to run the TestSuite.

Conclusion

Embedded JBoss seems to be an attractive solution for EJB3 testing with JUnit. For now, only the internal java DB is used for persistence but it seems possible to configure an other database like Mysql. I will try to cover this feature in a new post.

The JUnit4 framework has not been tested and can be evaluated in an other post.

Mots clés Technorati : ,,,

Saturday, December 01, 2007

Friday, November 16, 2007

JDeveloper and Jad

JDeveloper does not decompile the class file automaticaly. When you drop a .class in the editor, jdev only provides the class description with methods...not the java code.

Jdev configuration file located in <jdev-home>/jdev/bin/jdev.conf has to be modified to allow jad to be taken into account for decompilation. The following line has to be added at the end of the jdev.conf file.

AddVMOption -Djcncmd=c:/jadnt158/jad.exe -p -b -ff -nl -pi99999 -space -t2 -noinner

Take care to modify the jad path !

GWT and Gears, automatic manifest generation

When you create an application with GWT and Gear (http://code.google.com/p/gwt-google-apis/), a manifest has to be created to manage offline content.
Depending the targeted browser (Safari, IE, Gecko1.8, Gecko, Opera) GWT generates specifics files for html and javascript.
To minimize the manifest management (and the downloading process) a dedicated manifest has to be created for each browser.
A solution is to use a ServletFilter. This filter will automaticaly return the manifest regarding user's browser.
The filter configuration manages manifest version and resources to be included in the manifest (.css, .html, .gif...).
This filter will only be trigged when the manifest file is requested by Gears.


I hope this solution can help..
Don' t hesitate to post any comments or modifications.

FilterServlet code:



package com.alu.gwt.server;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
 
public class GearManifestFilter implements Filter {
    public static final String EXTENSIONS = "extensions";
    public static final String MANIFEST_VERSION = "manifestVersion";
    
    public static final String OPERA_PATTERN = "'opera'],";
    public static final String SAFARI_PATTERN = "'safari'],";
    public static final String GECKO1_8_PATTERN = "'gecko1_8'],";
    public static final String GECKO_PATTERN = "'gecko'],";
    public static final String IE6_PATTERN = "'ie6'],";
 
    public static final String OPERA = "opera";
    public static final String SAFARI = "safari";
    public static final String GECKO1_8 = "gecko1_8";
    public static final String GECKO = "gecko";
    public static final String IE6 = "ie6";
 
    protected String _operaCacheFilename;
    protected String _safariCacheFilename;
    protected String _gecko1_8CacheFilename;
    protected String _geckoCacheFilename;
    protected String _ie6CacheFilename;
 
    protected String _extensions;
 
    protected String _manifestVersion = "1.0";
 
    protected Map _manifestMap = new HashMap();
 
    public static class GearRegexpFilenameFilter implements FilenameFilter {
        private String _regexpFilter;
        private Pattern _regexpPattern;
 
        GearRegexpFilenameFilter(String regexpFilter) {
            _regexpFilter = regexpFilter;
            _regexpPattern = Pattern.compile(_regexpFilter);
        }
 
        public boolean accept(File dir, String name) {
            if ((name.indexOf(".cache.") == -1) && 
                (name.indexOf("-xs.nocache.") == -1)) {
                Matcher matcher = _regexpPattern.matcher(name);
                return matcher.find();
            } else
                return false;
        }
    }
 
    public static class GearResourcesExplorer {
        List _result = new ArrayList();
 
        public GearResourcesExplorer() {
 
        }
 
        public List list(File directory, FilenameFilter filter, 
                         boolean recursive, boolean root) {
            if (recursive) {
                File[] dirs = directory.listFiles(new FileFilter() {
 
                            public boolean accept(File pathname) {
                                if (pathname.isDirectory())
                                    return true;
                                return false;
                            }
                        });
                for (int i=0; i < dirs.length; i++)
                    list(dirs[i], filter, recursive, false);
            }
            
            String[] res = directory.list(filter);
            for (int i = 0; i < res.length; i++) {
                if (root) {
                _result.add(res[i]);
                } else {
                    _result.add(directory.getName() + File.separator + res[i]);
                }
            }
            return _result;
        }
    }
 
    public GearManifestFilter() {
    }
 
    public void init(FilterConfig filterConfig) {
        _extensions = filterConfig.getInitParameter(EXTENSIONS);
        _manifestVersion = filterConfig.getInitParameter(MANIFEST_VERSION);
        System.out.println("Extensions: " + _extensions);
        String realPath = filterConfig.getServletContext().getRealPath("/");
        System.out.println("Path:" + realPath);
        File dir = new File(realPath);
        
        List resourceFilenames = new GearResourcesExplorer().list(dir, new GearRegexpFilenameFilter(_extensions), true, true);
        
        for (int i = 0; i < resourceFilenames.size(); i++) {
            System.out.println("File: " + resourceFilenames.get(i));
        }
 
        FilenameFilter noCacheFilter = new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    if (name.endsWith(".nocache.js") && 
                        !name.endsWith("-xs.nocache.js"))
                        return true;
                    return false;
                }
            };
        String[] noCacheFilenames = dir.list(noCacheFilter);
        if (noCacheFilenames.length != 1) {
            System.out.println("Error !!");
        } else {
            getResourcesAccordingUserAgent(realPath + noCacheFilenames[0]);
        }
 
        _manifestMap.put(SAFARI, 
                         createManifest(_safariCacheFilename, _manifestVersion, 
                                        resourceFilenames));
        _manifestMap.put(GECKO, 
                         createManifest(_geckoCacheFilename, _manifestVersion, 
                                        resourceFilenames));
        _manifestMap.put(GECKO1_8, 
                         createManifest(_gecko1_8CacheFilename, _manifestVersion, 
                                        resourceFilenames));
        _manifestMap.put(OPERA, 
                         createManifest(_operaCacheFilename, _manifestVersion, 
                                        resourceFilenames));
        _manifestMap.put(IE6, 
                         createManifest(_ie6CacheFilename, _manifestVersion, 
                                        resourceFilenames));
    }
 
    protected void getResourcesAccordingUserAgent(String noCacheFilename) {
        System.out.println(noCacheFilename);
        BufferedReader in = null;
        StringBuffer buffer = new StringBuffer();
        try {
            in = new BufferedReader(new FileReader(noCacheFilename));
            String currentLine;
            while ((currentLine = in.readLine()) != null) {
                buffer.append(currentLine);
            }
            in.close();
        } catch (IOException e) {
            System.out.println("Error");
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
 
        _operaCacheFilename = getFilename(buffer, OPERA_PATTERN);
        _geckoCacheFilename = getFilename(buffer, GECKO_PATTERN);
        _gecko1_8CacheFilename = getFilename(buffer, GECKO1_8_PATTERN);
        _safariCacheFilename = getFilename(buffer, SAFARI_PATTERN);
        _ie6CacheFilename = getFilename(buffer, IE6_PATTERN);
 
        System.out.println("OperaFilename: " + _operaCacheFilename);
        System.out.println("GeckoFilename: " + _geckoCacheFilename);
        System.out.println("Gecko1_8Filename: " + _gecko1_8CacheFilename);
        System.out.println("SafariFilename: " + _safariCacheFilename);
        System.out.println("IE6Filename: " + _ie6CacheFilename);
 
    }
 
    protected String getFilename(StringBuffer noCacheStringBuffer, 
                                 String agentPattern) {
        String noCacheString = noCacheStringBuffer.toString();
        int beginIndex = 
            noCacheString.indexOf(agentPattern) + agentPattern.length() + 1;
        int endIndex = noCacheString.indexOf("'", beginIndex);
        String filename = noCacheString.substring(beginIndex, endIndex);
        return filename;
    }
 
    protected String createManifest(String userAgentHash, String version, 
                                    List resourceFilenames) {
        StringBuffer manifestBuffer = new StringBuffer("{\n");
        manifestBuffer.append("\"betaManifestVersion\": 1,\n");
        manifestBuffer.append("\"version\": \"");
        manifestBuffer.append(version);
        manifestBuffer.append("\",\n");
        manifestBuffer.append("\"entries\": [\n");
 
        manifestBuffer.append("{ \"url\": \"");
        manifestBuffer.append(userAgentHash + ".cache.js\"");
        manifestBuffer.append("},\n");
        manifestBuffer.append("{ \"url\": \"");
        manifestBuffer.append(userAgentHash + ".cache.html\"");
        manifestBuffer.append("},\n");
 
        for (int i = 0; i < resourceFilenames.size() - 1; i++) {
            manifestBuffer.append("{ \"url\": \"");
            manifestBuffer.append(resourceFilenames.get(i));
            manifestBuffer.append("\"},\n");
        }
        manifestBuffer.append("{ \"url\": \"");
        manifestBuffer.append(resourceFilenames.get(resourceFilenames.size() - 1));
        manifestBuffer.append("\"}\n");
        manifestBuffer.append("]\n");
        manifestBuffer.append("}\n");
        System.out.println("Manifest: " + manifestBuffer.toString());
        return manifestBuffer.toString();
    }
 
    public void doFilter(ServletRequest servletRequest, 
                         ServletResponse servletResponse, 
                         FilterChain filterChain) throws IOException, 
                                                         ServletException {
        HttpServletRequest request = ((HttpServletRequest)servletRequest);
        String userAgentString = request.getHeader("user-agent");
        System.out.println("UserAgent: " + userAgentString);
        String userAgent = getUserAgent(userAgentString);
        ServletOutputStream out = servletResponse.getOutputStream();
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        response.setContentType("application/jsonrequest");
        out.println((String)_manifestMap.get(userAgent));
    }
 
    protected String getUserAgent(String userAgentString) {
        if (userAgentString.indexOf("IE6") != -1 || 
            userAgentString.indexOf("MSIE 7") != -1) {
            System.out.println("IE6 or more detected");
            return IE6;
        } else if (userAgentString.indexOf("MOZILLA") != -1) {
            System.out.println("MOZILLA detected");
            return GECKO;
        } else if (userAgentString.indexOf("MOZILLA") != -1) {
            System.out.println("MOZILLA detected");
            return IE6;
        }
        return SAFARI;
    }
 
    public void destroy() {
    }
 
}

web.xml:




<filter>
<filter-name>gearManifestFilter</filter-name>
<filter-class>com.alu.gwt.server.GearManifestFilter</filter-class>
<init-param>
<description>Resources to be include in manifest</description>
<param-name>extensions</param-name>
<param-value>.htm|.css|.html|.js|.gif|.jpg</param-value>
</init-param>
<init-param>
<description>manifest version</description>
<param-name>manifestVersion</param-name>
<param-value>1.0</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>gearManifestFilter</filter-name>
<url-pattern>/manifest.json</url-pattern>
</filter-mapping>

Frederic shared items