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 !

iPhone: SQLite e singleton

iphone-guitarQuesto è il mio articolo sull’iphone e per l’occasione volevo parlare dell’utilizzo di SQLite all’interno della gestione del pattern singleton. SQLite è un insieme di librerie che implementano un motore di database self-contained che per la sua leggerezza e compatibiltià con gli standard SQL risulta essere il database più diffuso. Siccome è molto leggero richiede poche risorse, quindi ha ha senso che venga utilizzato all’inteno di dispositivi embeeded quali ad esempio l’iphone. l’iPhone implemente le librerie di SQLite fornendo così il supporto al database.

Passiamo subito ad un esempio pratico dando per scontato un conoscenza di base del linguaggio objective-c.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
//
//  DBHandle.h
//
//  Created by mulp on 11/05/10.
//
 
#import <Foundation/Foundation.h>
#import <sqlite3.h>
 
/* Define database name */
extern NSString * const databaseName;
 
@interface DBHandle : NSObject {
    /* Database variables */
    NSString *databasePath;
 
    /* Database handle */
    sqlite3 *dbconn;
}
 
@property (nonatomic, retain) NSString *databasePath;
 
/** 
 * Get singleton instance of this class, or rather
 * the only database handle.
 */
+(DBHandle*) getInstance;
 
/**
 * Connect to database.
 */
-(int) connectDB: (NSString*) path;
 
/**
 * Disconnect to database.
 */
-(void) disconnectDB;
 
/**
 * Check if the SQL database has already been saved to the users phone, 
 * if not then copy it over.
 */
-(void) checkAndCreateDatabase;
 
-(NSMutableArray*) getList;
@end

Analizziamo il file header della classe che sara DBHandle. La definizione dello header è molto semplice consiste in una costante che indica il nome del database, i classici metodi di connessione e disconnessione dal db. Un metodo (statico) per indenderci quello che ha il segno + davanti, che può essere invocato senza aver bisogno di instanziare la classe, che permette di ottenere l’istanza singleton della classe. Ed infine un metodo che verifica la presenza del database e all’occorrenza lo crea ed un metodo che restituisce la lista di elementi presenti all’interno di una tabella di esempio. Esistono altri metodi che non compaiono all’interno della definizione del file header in quanto privati alla classe DBHandle che andiamo a vedere.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//
//  DBHandle.m
//
//  Created by mulp on 11/05/10.
//
 
#import "DBHandle.h"
 
// Usage: [[DBHandle getInstance] method_name];
 
@implementation DBHandle
 
@synthesize databasePath;
 
static DBHandle *instance = nil;
NSString * const databaseName = @"devme.sql";
 
+(DBHandle*) getInstance {
    @synchronized([DBHandle class]) {
	if (!instance)
	    [[self alloc] init];
	    return instance;
	}
    return nil;
}
 
+(id) alloc {
    @synchronized([DBHandle class]) {
	NSAssert(instance==nil, @"Attempted to allocate a second instance of a singleton");
	instance = [super alloc];
	return instance;
    }
    return nil;
}
 
-(id) init {
    self = [super init];
    if (self != nil) {
	[self checkAndCreateDatabase];
    }
    return self;
}
 
-(id) retain {
    return self;
}
 
-(int) connectDB:(NSString *)path {
    // Open the database.
    int status = sqlite3_open([path UTF8String], &dbconn);
    if (status != SQLITE_OK) {
        // Even though the open failed, call close to properly clean up resources.
        sqlite3_close(dbconn);
        NSAssert1(0, @"Failed to open database with message '%s'.", sqlite3_errmsg(dbconn));
        // Additional error handling, as appropriate...
    }
    return status;
}
 
-(void) disconnectDB {
    sqlite3_close(dbconn);
}
 
-(void) checkAndCreateDatabase{
 
    // Get the path to the documents directory and append the databaseName
    NSArray *homePaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *homeDir = [homePaths objectAtIndex:0];
    self.databasePath = [homeDir stringByAppendingPathComponent:databaseName];
 
    BOOL success;
 
    // Create a FileManager object, we will use this to check the status
    // of the database and to copy it over if required
    NSFileManager *fileManager = [NSFileManager defaultManager];
 
    // Check if the database has already been created in the users filesystem
    success = [fileManager fileExistsAtPath:databasePath];
 
    // If the database already exists then return without doing anything
    if(success) return;
 
    // If not then proceed to copy the database from the application to the users filesystem
 
    // Get the path to the database in the application package
    NSString *databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:databaseName];
 
    // Copy the database from the package to the users filesystem
    [fileManager copyItemAtPath:databasePathFromApp toPath:databasePath error:nil];	
    [fileManager release];
}
 
-(NSMutableArray*) getItemsList {
    NSMutableArray * container = [[NSMutableArray alloc] init];
 
    NSLog(@"Path is: %@", databasePath);
    // Open the database from the users filessytem
    if(sqlite3_open([self.databasePath UTF8String], &dbconn) == SQLITE_OK) {
	// Setup the SQL Statement and compile it for faster access
	const char *sqlStatement = "SELECT strftime('%s', publish_days) / 86400, title FROM devme ORDER BY publish_days DESC";
	sqlite3_stmt *compiledStatement;
 
	if(sqlite3_prepare_v2(dbconn, sqlStatement, -1, &compiledStatement, NULL) != SQLITE_OK) {
	    NSLog(@"Error: failed to prepare stmt with message %s", sqlite3_errmsg(dbconn));
	}
 
	DevMe *devme;
	while(sqlite3_step(compiledStatement) == SQLITE_ROW) {
	    devme = [[Devme alloc] init];
	    // Read the data from the result row
	    devme.pdate = [NSNumber numberWithInt:sqlite3_column_int(compiledStatement, 0)];
	    devme.title = [NSString stringWithUTF8String:(char *)sqlite3_column_text(compiledStatement, 1)];
	    [container addObject:devme];
	}
 
	// Release the compiled statement from memory
	sqlite3_finalize(compiledStatement);
	[devme release];
    }
    return container;
}
 
@end

Come vedete anche la definizione della classe è molto semplice. Osserviamo subito la definizione di una variabile static che rappresenta l’istanza della classe singleton. Per ottenerla basta richiamare il metodo getInstance il quale alloca materialmente la memoria per l’oggetto, e lo inizializza invocanto il metodo init. All’interno dell’inizializzazione viene invocato il metodo checkAndCreateDatabase. SQLite memorizza i dati all’interno di un file il quale viene creato al primo accesso al database. Il metodo verifica la presenza del database e se non lo trova crea materialmente il file e lo rende disponibile all’uso. La catena di metodi consente quindi di allocare e inizializzare l’istanza della classe DBHandle e restituirla al chiamante. Sull’istanza della classe sarà poi possibile invocare i metodi di connessione e disconnessione al database e quindi l’invocazione del metodo che restituisce la lista di elementi presenti all’interno di una tabella di esempio del database. Una possibile invocazione potrebbe essere la seguente:

dbhandle = [DBHandle getInstance];
[dbhandle connectDB:@"devme.sql"];
NSMutableArray *datasource = [dbhandle getItemsList];
[dbhandle disconnectDB];

Il metodo getItemsList esegue una query sul database e preleva i 2 attributi della tabella nel nostro esempio devme. I 2 attributi estratti vengono assegnati alle proprietà dell’oggetto Devme il quale viene poi aggiunto all’interno dell’array container. Quindi in definitiva il container contiene una lista di oggetti del tipo Devme.

In questo modo utilizzando un singleton si ha la possibilità da qualunque punto della nostra applicazione di accedere all’unica istanza della classe manager del database evitando così eventuali errori di gestione della connessione e ottimizzando l’accesso al database stesso. Nei prossimi articoli vedremo esempi di utilizzo di oggetti grafici di base messi a disposizione dall’SDK dell’iphone.

Stay tuned !

WordPress Themes