Category: it’s Spring !

Live Search con Spring e JQuery

live_searchTempo fa in questo articolo abbiamo visto come fare ad integrare il supporto Ajax all’interno del contesto di Spring utilizzando il framework DWR. In questi giorni invece ho deciso di non utilizzare DWR nell’applicazione a cui sto lavorando perché non volevo aggiungere dell’overhead. Per cui ho deciso di risolvere il problema con i soli strumenti messi a disposizione sia da Spring che da javascript.

Per prima cosa individuiamo la libreria javascript che fa per noi, provate ad indovinare cosa useremo ? JQuery esatto, il framework Javascript ormai famoso….rimando al sito per i dettagli di implementazione. Ciò detto passiamo a Spring, decidiamo subito che sarà un controller ad hoc che si occuperà della live search, eseguendo la query sulla nostra tabella e restituendo i risultati in una collection di oggetti noti.
Nel file devme-servlet.xml, file di definizione dei componenti spring della nostra applicazione, andiamo a definire il mapping con il nostro controller: 

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
	<value>
	    /live.me=liveResolver
	</value>
    <property name="alwaysUseFullPath" value="true"/>
</bean>
 
<bean id="liveResolver" class="it.devme.live.LiveSearch">
	<property name="liveManager"><ref bean="liveManager"/></property>
</bean>

In questo modo ogni richiesta all’indirizzo live.me verrà gestita dal nostro controller liveResolver. Passiamo al controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class LiveSearch extends BaseCommandController {
 
    private LiveSearchManagerImpl liveManager;
    public void setLiveManager(LiveSearchManagerImpl liveManager) {
	this.liveManager = liveManager;
    }
 
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
	Object lsid = request.getParameter("lsid");
	Object q = request.getParameter("q");
	if (lsid==null || q==null) return new ModelAndView();
 
	int live = Integer.parseInt(lsid.toString());
	String key = q.toString();
	List result = null; 
	switch (live) {
	    // searchOnCity
	    case 1:
    	        result = liveManager.searchOnSchoolsData(key);
		if (result==null) return new ModelAndView();
		    break;
	   default:
		return new ModelAndView();
	}
 
	String content = "";
	for (String item : result) {
	     content += item;
	}
	return new ModelAndView(new LiveSearchView(content));
    }
}

Come si può vedere la classe in se è molto semplice.
Estende BaseCommandController per il quale è stato fatto override del metodo handleRequestInternal(HttpServletRequest request, HttpServletResponse response) così da gestire in modo personalizzato la richiesta. Osserviamo che all’interno della classe c’è l’inject della classe manager impl, ovvero il gestore del database che si preoccupa di eseguire il metodo di interrogazione e quindi di restituire i risultati impacchettando gli oggetti all’interno di una Collection.

/**
 * Effettua una ricerca sulla tabella target delle scuole.
 * 
 * @param key, chiave da ricercare
 * @return result, stringa dei risultati formattati secondo 
 */
@SuppressWarnings("unchecked")
public List<string> searchOnSchoolsData(String key) {
    try {
	String sql = 	"SELECT name, address, city " +
			"FROM \"Schools\" " +
			"WHERE name ILIKE ? OR address ILIKE ? OR city ILIKE ?" +
			"ORDER BY name, city";
 
	List<string> items = (List<string>) jdbcTemplate.query(sql,
			new Object[]{ Utility.wrapWithWildcards(key.trim()), 
                                      Utility.wrapWithWildcards(key.trim()),
                                      Utility.wrapWithWildcards(key.trim()) },
		        new RowMapper() {
			    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
		                String result = "";
				result += rs.getString("name") + "|" + 
					  rs.getString("address") + "|" +
					  rs.getString("city") + "\n";
				return result;
			    }
	});
	log.info("Search on schools with key "+key);
	return items;
    } catch (EmptyResultDataAccessException e) {
	return null;
    }  catch (DataAccessException dae) {
	dae.printStackTrace();
	log.info("Error while searching on schools.");
	return null;
    }
}

Per completezza riportiamo il metodo del managerImpl che estrae i dati dal DB. Si osservi che la collection restituita altro non è che una lista di stringhe in cui ciascun elemento è l’insieme dei dati delle scuole separati dal carattere separatore ‘|’ e terminati da uno ‘\n’.
Il metodo handleRequestInternal estrae dalla request 2 parametri che sono: lsid e q. Il primo serve ad indicare quale metodo di recupero dei dati eseguire; il secondo indica la chiave della ricerca. Nel nostro esempio c’è solo un metodo per il recupero dei dati, ma nulla vieta di aggiungerne quanti più se ne desidera.

I dati estratti vengono concatenati all’interno di una stringa passata al costruttore della view personalizzata (si veda questo post), come segue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * @author mulp @ devme
 *
 */
public class LiveSearchView extends AbstractView {
 
    private String content;
 
    public LiveSearchView(String content) {
	this.content = content;
    }
 
    @Override
    protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
	byte [] bytes = content.getBytes();
        // Write content type and also length (determined via byte array).
        response.setContentType(getContentType());
        response.setContentLength(bytes.length);
 
        // Flush byte array to servlet output stream.
        ServletOutputStream out = response.getOutputStream();
        out.write(bytes);
        out.flush();		
    }
 
}

La view nel suo metodo renderMergedOutputModel realizza la scrittura di quanto estratto dal db all’interno della response, nella quale è stato precedentemente fissato il tipo e la lunghezza del contenuto. Questa azione comporta materialmente la comparsa nella pagina web del contenuto estratto, più precisamente viene intercetatto dal metodo javascript che formatta opportunamente il risultato. Di seguito vediamo i javascript che servono per realizzare la live search e lo scorcio di pagina che contiene il campo di testo sul quale avviene la live search.

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
<script type="text/javascript" src="/script/jquery/jquery.autocomplete.js"></script>
 
<script type="text/javascript">
$(document).ready(function() {
	function formatItem(row) {
		return row[0] + "," + row[2] + "," + row[1];
	}
	function formatResult(row) {
		return row[0] + "," + row[1] + "," + row[1];
	}
 
	$("#ls_school").autocomplete('/live.me?lsid=1', {
		width: 400,
		matchContains: true,
		formatItem: formatItem,
		formatResult: formatResult,
		max: 100
	});
});
 
.....
.....
<input type="text" name="ls_school" id="ls_school" />
.....
....

Il codice Javascript di sopra è il binding con il componente JQuery dell’autocomplete (compreso il css) il quale trasforma la digitazione dei caratteri all’interno del campo di testo in una chiamata HTTP all’indirizzo http://www.devme.it/live.me?lsid=1&q=devme. Lato back-end risponde il nostro controller il quale effettua la ricerca e restituisce il risultato come descritto sopra. Infine il risultato verrà formattato a modi tendina da JQuery.
Come si vede è molto semplice gestire la live search o, in generali, componenti che richiedono interazione Ajax con questa tecnica. Spero di riuscire a mostrare altri esempi diversi, più avanti in altri articoli.
Stay tuned !

Spring Web Flow tutto d’un fiato ! [Part 2/2]

spring-webflowRieccoci qui per continuare il discorso iniziato nel post precedente su SFW, il framework per la creazione di applicazioni web. Dopo aver definito gli elementi facenti parte della definizione di un flusso (flow) vediamo come è possibile definirlo, attraverso la sua specifica XML. Come detto in precedenza, il flow ha inizio grazie al flow executor un componente interno di SWF. L’executor da inizio al flow a partire dallo stato configurato come attributo nel tag root della definizione del flow:

<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow
      http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
      start-start="loginState">
	<-- Add your definition here -->
</flow>

loginState rappresenta lo stato iniziale da cui avrà inizio l’esecuzione del flow. Come già detto gli stati sono gli elementi di base della definizione di un flow, in cui è possibile transire da uno stato ad un’altro effettuando delle operazioni, dei controlli o visualizzare dell’output. Tra i possibili stati che possono essere utilizzati vi è il View State, il quale viene utilizzato per visualizzare o chiedere informazioni agli utenti. Una view può essere una qualunque pagina JSP, ma può anche essere una qualunque view supportata da Spring MVC. Di seguito viene riassunto l’elenco di possibili elementi figli del view state:

  • <attribute>, dichiara un attributo che descrive lo stato.
  • <binder>, usato per configurare un custom form binding.
  • <exception-handler>, riferisce un bean che implementa un FlowExecutionExceptionHandler che gestisce tutte le eccezioni sollevate nello stato corrente.
  • <on-entry>, indica quali azioni devono essere eseguiti quando si entra in questo stato.
  • <on-exit>,  indica quali azioni devono essere eseguiti quando si esce da questo stato.
  • <on-render>. indica quali azioni devono essere eseguiti quando si visualizza questo stato.
  • <secured>, viene utilizzato assieme a Spring Security per restringere l’accesso a questo stato.
  • <transition>, definisce un percorso da questo stato ad un’altro basato sul verificarsi di un evento o un’eccezione.
  • <var>, dichiara una variabile.

Ciò detto, il modo più semplice per dichiarare un View State è il seguente:

<view-state id="login"/>

Il tag di sopra definisce una view logica, ovvero non associata a nessuna pagina fisica, da cui non sarà possibile transire in nessun’altro stato. Per associare la view, il nome logico definito dall’attributo id ad una pagina jsp è necessario aggiungere l’attributo view,

<view-state id="login" view="loginForm"/>

in questo modo la view è associata alla pagina loginForm.jsp, la quale verrà visualizzata al caricamento della view. Nota: sarà possibile evitare di dover specificare l’estensione dei file associati alle view, configurando opportunamente Spring MVC View Resolver.

Ma vediamo un esempio un pò più completo:

<view-state id="login" view="loginForm">
  <transition on="accountEntered" to="homeUser" />
  <transition to="endState" on="cancel" />
</view-state>

l’esempio di sopra definisce due elementi figli del tag view, elementi transition. Il primo dei due indica una transizione dallo stato corrente allo stato homeUser, al verificarsi incondizionato dell’evento accountEntered. Il secondo definisce una transizione dallo stato corrente allo stato endState al verificarsi dell’evento cancel. Come definiamo questo tipo di eventi ? Molto semplicemente….lo scatenarsi degli eventi avviene direttamente dalle view fisiche associate. Consideriamo la view loginForm.jsp associata alla view logica login come segue:

<form:form>
   <!-- HTML form definition here -->
   <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"/>
   <input type="submit" name="_eventId_accountEntered" value="Entra" />
   <input type="submit" name="_eventId_cancel" value="Annulla" />
</form:form>

la pagina web contiene tra gli altri elementi che ospiteranno i dati di accesso dell’utente, il pulsante di submit della pagina e un campo hidden. Quest’ultimo è richiesto per riconoscere l’id del flow in cui ci si trova ed è sempre necessario. I pulsanti di submit, oltre al loro classico funzionamento servono anche a scatenare gli eventi che abbiamo definito nel file di flow. Si noti il nome del pulsante submit Entra che invia i dati del form al server e serve per effettuare l’autenticazione dell’utente. In particolare il nome del pulsante ha un prefisso _eventId_ il quale indica a SWF che alla pressione del bottone dovrà scatenare un evento il cui nome è ciò che segue il prefisso, nel nostro caso accountEntered. In questo modo è possibile catturare l’evento nel file di definizione del flow. Stesso discorso vale per il bottone cancel.
E’ possibile anche scatenare un’evento attraverso un link, ad esempio:

<a href="${flowExecutionUrl}&_eventId=accountEntered">Entra</a>

Altro modo consiste nel creare un acampo hidden all’interno del form con name _eventId.

Dopo aver analizzato il ViewState che permette all’utente di essere coinvolto all’interno del flow, passiamo a vedere l’ActionState il quale definisce alcuni elementi che permettono di effettuare delle elaborazioni all’interno del flow. L’elenco dei nodi figli dell’action state è il seguente:

  • <attribute>, dichiara un attributo che descrive lo stato
  • <evaluate>, valuta un’espressione assegnando opzionalmente il risultato.
  • <exception-handler>, referenzia un bean che implementa FlowExecutionExceptionHandler il quale gestisce le eventuali eccezioni per lo stato corrente.
  • <on-entry>, definisce un’azione la quale viene eseguita all’ingresso dello stato.
  • <on-exit>, definisce un’azione la quale viene eseguita all’uscita dello stato.
  • <render>, richiede che la prossima view renderizzi un frammento di contenuto.
  • <secured>, restringe l’accesso allo stato corrente richiedendo l’inserimento degli attributi utente.
  • <set>, imposta una variabile all’interno dello scope del flow.
  • <transition>, definisce una transizione verso uno stato al verificarsi di un evento. Lo stato di destinazione può coincidere con lo stato corrente.

Quindi vediamo un’esempio d’uso dell’ActionState:

<action-state id="homePage">
	<evaluate expression="devME.authenticateUser(username, password)" />
	<transition to="home" on="userOK" />
	<transition to="loginForm" on="userKO" />
</action-state>

Il codice di sopra definisce il seguente comportamento. Dopo che l’utente ha inserito i propri dati di accesso all’interno del form e clicca sul pulsante Entra viene scatenato l’evento accountEntered, il quale prevede una transizione nello stato homeUser, un’ActionState. All’interno viene valutata un’espressione la quale esegue un metodo di autenticazione dell’utente sulla base dei dati forniti. Lo stesso metodo scatena 2 eventi diversi: userOK se l’utente è stato autenticato, dal quale si transisce verso la home dell’utente. L’evento userKO se l’utente non è stato autenticato e quindi si ritorna alla view di login. Come vedete è molto semplice e intuitivo.
Stay tuned.

WordPress Themes