MokaByte Numero 32  -  Luglio Agosto 99 
 
LightServer 
di 
Ugo Chirico
Una tecnica per la generazione dinamica di pagine web


Una soluzione alternativa piuttosto interessante che mostra come realizzare, con un sistema alternativo ai classici client server, un motore   per la generazione di contenuti dinamici
La generazione dinamica di pagine web rappresenta una delle funzionalità più interessanti tra quelle fornite dai server HTTP. La Common Gateway Interface (CGI) fu la prima tecnologia sviluppata. Molto diffuse sono le Active Server Page (ASP) fornite dai server della Microsoft. Molto interessanti e davvero flessibili ed eleganti sono le Servlet Java.
Tuttavia, sebbene abbiano delle caratteristiche che le contraddistinguono e che ne hanno determinato il successo, sono tutte strettamente legate alla piattaforma ed alle caratteristiche del server. Per le CGI è necessario avere un server che consenta l'esecuzione dello script in perl, o in un altro linguaggio interpretato dal server, o direttamente come file eseguibile. Per le ASP è necessario un server Microsoft o dei plug-in. Infine per eseguire le Servlet, sebbene il codice scritto in Java sia indipendente dalla piattaforma, è necessario installare sul server un  servlet-engine.
Così, anche quando il risultato che si vuole ottenere è davvero molto semplice, è necessario valutare tutti gli aspetti relativi alle caratteristiche della piattaforma e del server per scegliere la tecnologia migliore. Se poi malauguratamente il server non supporta nessuna delle tre tecnologie, come capita in quei provider che forniscono spazio web gratuito, non c'è soluzione.
 Quest'articolo si propone di presentare un'alternativa "leggera" alle suddette tecnologie che, se pur con delle limitazioni (altrimenti non sarebbe "leggera"), consenta la generazione dinamica di pagine web, tralasciando completamente gli aspetti relativi al server. A dimostrazione della tecnica si presenterà infine un motore di ricerca per siti web.
 
 

L'idea di base

L'idea di base prevede un applet da inserire nella pagina contenente la form per l'immissione dei dati che determineranno la pagina da generare, poche righe di codice in JavaScript, ed eventualmente uno o più file, localizzati sul server dal quale è stata scaricata l'applet, da usare ad esempio come template per la pagina, o come semplice database contenente ulteriori informazioni da elaborare durante la generarazione della pagina (Figura 1).
L'unica limitazione sta nell'impossibilità di creare ex-novo o di aggiornarne file sul server, limitazione imposta dal protocollo HTTP. Tuttavia, tale limitazione potrebbe essere superata qualora il server fornisse anche un servizio FTP.
 
 


Figura 1 Paradigma di computazione









Il paradigma di computazione è il seguente: la form raccoglie i dati; il codice JavaScript costruisce con essi la query e la passa all'applet chiamando il metodo postQuery; quest'ultima elabora la query, legge il file di dati sul server per prendere delle decisioni, legge eventualmente il template, e costruisce con esso la nuova pagina inserendola in una stringa che restituisce al chiamante; il codice JavaScript che riceve la pagina sottoforma di stringa, provvede a costruire una nuova finestra scrivendo in essa la pagina generata (Figura 1). 
 
 

Implementazione di un LightServer

Di seguito è riportata una possibile implementazione delle componenti necessarie a costruire un LightServer. 
Si mostreranno solo gli aspetti fondamentali, rimandando il lettore interessato ai file sorgenti allegati.
Il codice è stato scritto seguendo le specifiche del JDK 1.02 per consentire l'esecuzione dell'applet anche nei browser della vecchia generazione.
 
 

Classe LightServer

La classe LightServer è una classe astratta che implementa le funzionalità di base. È la base-class da cui derivare per costruire un server specializzato. 
 
public abstract class LightServer extends Applet


Il template della pagina risultante è passato all'applet nel parametro bodyattributes mediante il quale è possibile specificare gli attributi del tag <body>. Si è preferito non usare file esterni in qualità di template per semplificare la creazione della pagina. Tuttavia, per usi più complessi, si consiglia di costruire un serie di template che verrando scelti a run-time seguendo una particolare euristica e prevedendo, per ognuno di essi, diversi punti di iniezione in cui inserire i risultati della query.
 

 // Attributi del tag <body> della pagina risultante
 // È possibile impostare un semplice template per la pagina risultante
 // inserendo gli attributi del <body> in uno dei parametri dell'applet
 protected String m_strBodyAttributes;

 // Inizializza l'applet e imposta gli attributi del body 
 public void init()
 {
  super.init();
  m_strBodyAttributes = getParameter("bodyattributes");
 }


Il codice JavaScript invoca il metodo postQuery per passare la query al LightServer e per dare inizio alla computazione. La sintassi della query è la seguente: param1=valore1&param2=valore2…
 

 // Query impostata nella pagina html
 private String m_strQuery;

 // Questo metodo è chiamato dal codice JavaScript nella pagina html 
 // per impostare la query. 
 // La sintassi della query è la seguente: param1=valore1&param2=valore2…
 public final String postQuery(String strQuery)
 {
  // Imposta la query aggiungendo il carattere '&' all'inizio ed alla fine per 
  // facilitare l'estrazione delle coppie parametro-valore (vedi getParmValue)
  m_strQuery = "&" + strQuery + "&";

  String strResultPage;
  try
  {
   // Avvia la computazione
   strResultPage = executeQuery();
  }
  catch(LightServerException ex)
  {
   // In caso di errore previsto costruisce la pagina risultante
   strResultPage = ex.getMessagePage();
  }
  catch(Exception ex)
  {
   // In caso di errore imprevisto costruisce la pagina risultante
   strResultPage = getGenericExceptionPage(ex.toString());
  }

  return strResultPage;
 }


La computazione  è eseguita nel metodo executeQuery. Esso è un metodo astratto che deve essere implementato nelle classi derivate per definire il comportamento del server

 // Questo è il metodo che deve essere riscritto nella classe derivata
 // per implementare il proprio server
 protected abstract String executeQuery() throws LightServerException;

La funzione getParmValue consente di estrarre dalla query il valore di uno dei parametri

 // Interpreta la query e restituisce il valore del paramentro richiesto
 protected final String getParmValue(String strParmName) throws LightServerException

Infine la funzione getGenericExceptionPage, chiamata in caso di errore imprevisto, genera una pagina che mostra l'eccezione occorsa.

 // In caso di errore imprevisto costruisce una pagina contenente l'errore
 // Da sovrascrivere per generare una pagina di errore personalizzata 
 protected String getGenericExceptionPage(String strMsg)
Classe LightServerException

La classe LightServerException implementa un eccezione prevista dal LightServer. 

 public class LightServerException extends Exception

 // Attributi del tag <body> della pagina risultante (vedi LightServer)
 private String m_strBodyAttributes;

 // Costruttore 
 public LightServerException(String strMsg, String strBodyAttributes)
 {
  super(strMsg);
  m_strBodyAttributes = strBodyAttributes;
 }

Il metodo getMessagePage, restituisce una pagina che specifica l'errore occorso. Per implementare una pagina di errore personalizzata è necessario derivare da essa e sovrascrivere il suddetto metodo.

 // Genera una pagina contenente il messaggio dell'eccezione
 public String getMessagePage()
 {
  String strPage = 
   "<head><title>LightServer Error</title></head>\n "
    + "<body " + m_strBodyAttributes + ">\n" 
    + "<hr>\n"
   + "<h2 align='center'><strong>" + "The LightServer raised an exception"
   + "</strong></h2>\n"
   + "<br>\n"
    + "<p align='center'><strong>The message was: " + getMessage()
   + "</strong></p>\n"
    + "<hr>\n"
    + "</body>";

   return strPage;
 }
 
 
 

Il codice JavaScript e la pagina HTML

Il codice JavaScript fa da collante tra l'applet, la form di immissione dati e la pagina risultante.
Poichè i metodi di un applet possono essere invocati da JavaScript solo a caricamento avvenuto è necessario introdurre un controllo sul caricamento della pagina. La variabile bLoaded e la funzione loaded, invocata al verificarsi dell'evento onLoad del body, asservono a tale scopo.

 <body bgcolor="#FFFFFF" onLoad="loaded();">
 <script language="JavaScript">
 <!--
 // Indica l'avvenuto caricamento della pagina 
var bLoaded = false;

 // Invocato al verificarsi dell'evento onLoad del body 
function loaded()
{
 bLoaded = true;
}

La funzione invokeServer costruisce la finestra che ospiterà la pagina risultante, invoca il metodo postQuery del LightServer e, al termine della computazione, scrive nella finestra la stringa contenente la pagina.

// Invoca l'applet passandogli la query
function invokeServer(strQuery){
 if(!bLoaded)
 {
  // E necessario attendere il caricamento della pagina per essere sicuri
  // che l'applet è stata caricata ed inizializzata
  alert("Attendi il caricamento della pagina e riprova. Grazie.");
  return;
 }

 // Genera la pagina dei risualtati
 var resultWin = window.open("","_blank");

 // Estrae il documento
 var resultDoc = resultWin.document;

 // Scrive un messaggio di attesa
 resultDoc.write("<br><hr><center><H2>Attendere prego</H2></center><hr>");

 // Invoca il Server
 var strPage;
 strPage = document.MioServer.postQuery(strQuery);

 // A computazione terminata cancella il messaggio di attesa 
 resultDoc.clear();
 resultDoc.close();

 // Scrive il risultato della query nella pagina
 resultDoc.open("text/html");
 resultDoc.write(strPage);
 resultDoc.close();
}
 // -->
 </script>

L'applet va inserita della pagina impostando i valori dei parametri richiesti

<applet 
 code="MioServer.class" 
 codebase="MioURL"
 width="0" 
 height="0" 
 name="MioServer">
 <param 
  name="bodyattributes"
    value="bgcolor='#000080' background='BLUE_THIN_LINE.gif'">

--- eventuali altri parametri ---

</applet>

La form consente di raccogliere i dati necessari per costruire la query e di invocare il server alla pressione del tasto Submit

<form onSubmit="invokeServer('query=' + query.value)">
 <input type="text" name="query" value size="20"> 
   <input type="submit" name="submit" value="Submit"> 
</form>
</body>
</html>
 
 
 

Un motore di ricerca

A titolo di esempio, e per mostrare le potenzialità effettive del LightServer, è stato implementato un motore di ricerca per il web. Le prestazioni sono buone per siti di piccole e medie dimensioni, tuttavia non è stato ancora sperimentato in siti più grandi.
 
 

Classe WebSearchServer

La classe WebSearchServer implementa il motore di ricerca.

 public class WebSearchServer extends LightServer

La query deve essere nel formato "keyword=<keywords>". Supporta anche query formulate usando gli operatori logici AND e OR, rappresentati rispettivamente dai simboli '+' e '<spazio>'. Ad esempio sono valide le seguenti query:

"keyword=pasta+fagioli" oppure "keyword=java game"

Per la generazione della pagina dei risultati essa fa uso di un file di testo che rappresenta il database contenente le parole chiave relative alle pagine all'interno del sito e i riferimenti ad esse (il file index.txt in  ne è un esempio).

 // URL del database
 private URL m_url;

In questa implementazione un record di tale database è composto da cinque campi separati da {CR+LF} e ha il seguente formato:

URL della pagina
Titolo
Descrizione
Keywords (separate da virgole)
Separatore 

Esempio:

./Italiano/SearchLibIta.html
Search Library abstract.
Una libreria Java per la ricerca intelligente in un grafo
othello,linegame,java,applet,package,inference,depth,first,search,artificial,intelligence
-------------------------------------------------------

A causa della sua semplice struttura, il database può essere popolato agevolmente a mano. Tuttavia, soprattutto quando il sito è di grandi dimensioni, è consigliabile scrivere un piccolo robot che interpretando i tag nelle pagine popola il database riempiendo opportunamente i campi.
Il metodo init inizializza l'applet, legge il valore del parametro database contenente la URL del database e imposta il valore della variabile m_url

 // Stream di input per la lettura del database
 private DataInputStream m_DBStream;

 // Inizializzazione. Legge il valore del parametro dell'applet "database"
 // ed imposta la variabile m_url
 public void init()
 {
  super.init();

  // Legge la URL del database nel parametro "database"
  String strDBFile = getParameter("database");
  try
  {
   m_url = new URL(getCodeBase().toString() + strDBFile);
  }
  catch(MalformedURLException ex)
  {
   System.out.println("MalformedUrlException");
   m_url = null;
  }
 }

Il metodo executeQuery, implementa l'algoritmo per la scomposizione della query e per la ricerca delle parole chiave richieste all'interno del database. Infine costruisce la nuova pagina inserendo un item per ogni pagina trovata.
L'applet prevede due parametri aggiuntivi, table e image per specificare rispettivamente il formato della tabella ospitante gli items dei risultati e l'immagine da anteporre ad ogni item 

 // Implementa il l'algoritmo del server
 protected String executeQuery() throws LightServerException
 {
  // Controlla la URL
  if(m_url == null)
  {
   LightServerException lsEx = 
    new LightServerException("Malformed URL", m_strBodyAttributes);
   throw lsEx;
  }

  // Apre il database
  try
  {
   m_DBStream = new DataInputStream(m_url.openStream());
  }
  catch (IOException ex)
  {
   LightServerException lsEx = 
    new LightServerException(ex.toString(), m_strBodyAttributes);
   throw lsEx;
  }

  // Estrae dalla query il valore del parametro "keyword"
  String strKeywords = getParmValue("keyword");
  if (strKeywords == null)
  {
   // Genera una pagina vuota
   return nullPage();
  }

  // Vettore contenente i risultati della ricerca
  Vector resultVect = new Vector();

  // Verifica eventuali operatori booleani '+' per AND e '<spazio>' per OR
  // e scompone la query inserendo le keyword
  // Ricerca nel database le keywords e produce un vettore dei risultati
 

 N.B. A causa della lunghezza e pesantezza del codice per la  scomposizione della  query e per la ricerca nel database al fine di   popolare il vettore dei risultati, si rimanda al file sorgente allegato.

  // L'applet prevede due parametri aggiuntivi "table" e "image" per 
  // specificare le caratteristiche della tabella che ospiterà il database e 
  // l'URL dell'immagine da anteporre ad ogni item risultante dalla ricerca
  String strBKGTABLE = getParameter("table");
  if (strBKGTABLE == null)
  {
   strBKGTABLE = "";
  }

  // URL dell'immagine
  String strImage = getParameter("image");
  if (strImage == null)
  {
   strImage = "";
  }

  // Stringa contenente la pagina dei risultati
  String strPage;

  // Costruisce la pagina
  strPage = "<html>"
     + "<head><title>Search Result</title></head>\n "
     + "<body " + m_strBodyAttributes + ">\n" 
     + "<center><table " + strBKGTABLE + "><tr><td>\n"
     + "<center><h3 align='center'><strong>Search Result</strong></h3>\n"
     + "<h4 align='center'>Query: " + strKeywords + "</h4></center>\n"
     + "<hr>\n";

  // Inserisce nella pagina un item per ogni pagina trovata
  if(resultVect.size() > 0)
  {
   for (i = 0; i < resultVect.size(); i++)
   {
    strPage += "<p><img align='middle' src=" + strImage + "><strong>   " 
         + resultVect.elementAt(i) + "</strong></p>\n";
   } 
  }
  else
  {
   // Se il vettore dei risultati è vuoto non è stata trovata nessuna pagina
   // che corrisponde alla query data.
   strPage += 
    "<p><strong>No pages found that match the keywords</strong></p>\n";

  }

  // Termina la pagina dei risultati
  strPage += "<hr></td></tr></table></center>\n"
       + "</body></html>\n";

  return strPage;
 }

La pagina HTML con la form per la ricerca

La pagina HTML contenente la form per la ricerca deriva da quella presentata sopra. Di seguito sono riportate solo le parti di codice soggette a modifiche evidenziando in grassetto le variazioni apportate.

function invokeServer(strKeywords)
{
 if(!bLoaded)
 {
  // E necessario attendere il caricamento della pagina per essere sicuri
  // che l'applet è stata caricata ed inizializzata
  alert("Attendi il caricamento della pagina e riprova. Grazie.");
  return;
 }

 // Genera la pagina dei risualtati
 var resultWin = window.open("","_blank");

 // Estrae il documento
 var resultDoc = resultWin.document;

 // Scrive un messaggio di attesa
 resultDoc.write("<br><hr><center><H2>Attendere prego</H2></center><hr>");

 // Invoca il Server
 var strPage;
 strPage = document.WebSearchServer.postQuery(strKeywords);

 // A computazione terminata cancella il messaggio di attesa 
 resultDoc.clear();
 resultDoc.close();

 // Scrive il risultato della query nella pagina
 resultDoc.open("text/html");
 resultDoc.write(strPage);
 resultDoc.close();
}

Nel tag relativo all'applet è necessario impostare i valori dei parametri

<applet 
 code="WebSearchServer.class" 
 width="0" 
 height="0" 
 name="WebSearchServer">
 <param 
  name="bodyattributes"
    value="bgcolor='#000000' alink='#00FFFF'">
   <param 
    name="database" 
    value="index.txt">
   <param 
    name="table" 
    value="border='3' cellpadding='5' width='95%'">
   <param 
    name="image" 
    value="ptCyan.gif">
</applet>

La form prevede un campo text in cui inserire la parola da ricercare.

 <form onSubmit="invokeServer('keyword=' + Keywords.value)">
  <input type="text" name="Keywords" value size="20"> 
  <input type="submit" name="submit" value="Search"> 
 </form> 

(In allegato è riportata l'implementazione di tutte le componenti pronta all'uso)
 
 

Conclusioni

Il codice presentato è solo una tra le possibili implementazione della tecnica illustrata. Data la semplicità dell'idea esso può essere facilmente modificato per asservire ad esigenze particolari. 
L'esempio illustrato è stato testato con Netscape Navigator 4.5 e Internet Explorer 4.0. Date le sostanziali differenze tra i browser non è ipotizzabile che per versioni diverse siano necessarie delle modifiche nel codice JavaScript.
Le limitazioni della tecnica illustrata sono imputabili alle restrizioni imposte dal protocollo HTTP che non consente la creazione ex-novo di file sul server o l'aggiornamento degli stessi. Qualora il server fornisca anche un servizio FTP, è ragionevole pensare che, mediante i comandi del protocollo, sia possibile creare e aggiornare file sul server. Tuttavia questa soluzione rimane sul piano puramente teorico in quanto non è stata ancora sperimentata. Il prossimo passo prevede l'estensione della tecnica proprio in questa direzione.

 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it