JSF 2 e il versionamento delle risorse

In un post precedente abbiamo visto da vicino cosa significa “risorsa” per JSF 2, insieme alle convenzioni e agli strumenti necessari per gestirle. Una volta messo in piedi questo sistema, una domanda lecita è: ma come faccio a gestire il loro versionamento? Se modifico un css o uno script, come faccio ad essere sicuro che il browser dell’utente scarichi l’ultima versione e invalidi quella in cache? Rispondiamo a questa domanda in stile JSF 2!

Lo scopo

Normalmente si vede nello sviluppo web che i link a un foglio di stile o ad un JavaScript terminano con un query param del tipo:

<link type='text/css' rel='stylesheet' href='my-style.css?v=1.0' />
<script type='text/javascript' src='my-script.js?v=1.0'></script>

Ovviamente file statici come .css e .js non possono ricevere parametri. Allora a che serve? Immaginiamo la cache del browser come una mappa, la cui chiave è l’URL della risorsa e il valore è la risorsa stessa. In questo caso la chiave sarà rispettivamente:

  • my-style.css?v=1.0
  • my-script.js?v=1.0

Se cambio la versione delle risorse sul server, è facile capire che il loro URL cambierà, invalidando automaticamente le versioni presenti nei browser! Questo sistema sembra artigianale, ma in realtà è molto efficace! Vediamo come lo ha inglobato JSF 2.

Il sistema di versionamento secondo JSF 2

JSF 2 ha adottato lo stesso meccanismo di versionamento delle risorse: una volta introdotto, gli URL delle risorse sul client termineranno tutti come mostrato poco fa. La gestione però è automatica, non dobbiamo preoccuparci cioè di modificare le dichiarazioni dei tag outputStylesheet o outputScript, bensì di come sono organizzate le risorse sul file system!!

Prendiamo per esempio un header di pagina tale che:

<h:head>
  <h:outputStylesheet name="style_a.css" library="css"/>
  <h:outputScript name="script_a.js" library="js"/>
</h:head>

Esistono due approcci per organizzare le versioni, a fronte dello stesso codice html:

Versionamento per libreria

Organizziamo le risorse come in figura:

Versionamento per libreria

cioè, ogni libreria è formata da sottocartelle con nome numerico che ne rappresenta la versione. Le eventuali sottoversioni sono separate da underscore (_) (non dal punto mi raccomando!). Il ResourceHandler, automaticamente caricherà la versione più aggiornata per ogni libreria, ovvero:

  • la 1_0_1 per i css;
  • la 1_3 per i js.

Sul client, otterremo una cosa del tipo:

<head>
  <script type="text/javascript" src="/MyContextPath/javax.faces.resource/script_a.js.jsf?ln=js&amp;v=1_3"></script>
  <link type="text/css" rel="stylesheet" href="/MyContextPath/javax.faces.resource/style_a.css.jsf?ln=css&amp;v=1_0_1" />
</head>

Vantaggi? Ho organizzato le risorse per versione e nella pagina non devo modificare niente!
Svantaggi? Ogni file posto al di fuori delle cartelle “numerate” (cioè figlio diretto di css o js) non viene trovato.

Versionamento per singola risorsa

Se non vogliamo gestire un sistema di versionamento per libreria, è possibile farlo per singola risorsa, con una struttura alquanto bizzarra. Ammettiamo di voler controllare solo le versioni di stile_a.css: dovremo creare una cartella con questo nome e inserire i file css al suo interno nominati con la notazione delle versioni, come in figura:

Versionamento per risorsa

In pratica è la modalità precedente invertita: il nome del file css diventa quello della cartella e il numero delle versione, che prima era una cartella, adesso diventa il nome del file css.
Vantaggi? Dato che il versionamento è a livello di file, possono coesistere file versionati da quelli non versionati all’interno della stessa libreria (in questo caso, style.css viene risolto).
Svantaggi? Se proviamo ad usare entrambi i sistemi di versionamento, vince quello per libreria.

La via alternativa

Il sistema di versionamento per file può essere molto interessante quando importiamo nel nostro progetto stili o script di terze parti, la cui versione non è controllata da noi. Per quanto riguarda invece i nostri stili o script, creare file o cartelle diverse per ogni versione può essere un problema. Se infatti il nostro progetto è controllato da un Version Control System (CVS/SVN/GIT per intendersi), creare un nuovo file ad ogni versione frammenta la storia di quello stile o di quello script in più file, per cui tracciare i cambiamenti a ritroso, in caso di necessità, può essere fastidioso perché è necessario saltare tra più file.

Fortunatamente JSF 2 è molto flessibile, e ci permette di personalizzare il ResourceHandler come ci pare e piace. Torniamo quindi alle basi: vogliamo che venga aggiunta la versione ai link ai file, però senza impazzire con cartelle, sottocartelle o nomi numerici. Prendiamo spunto da come fa PrimeFaces e scriviamo il nostro ResourceHandler:

public class VersionedResourceHandler extends ResourceHandlerWrapper {

   private ResourceHandler wrapped;
   
   public VersionedResourceHandler(ResourceHandler wrapped) {
      this.wrapped = wrapped;
   }
   
   @Override
   public Resource createResource(String resourceName, String libraryName) {
      Resource resource = super.createResource(resourceName, libraryName);
      if (resource != null && libraryName != null && !libraryName.equalsIgnoreCase("javax.faces")) 
         return new VersionedResource(resource);
      else 
         return resource;
   }
   
   @Override
   public ResourceHandler getWrapped() {
      return wrapped;
   }
}

Questa classe ci permette di decorare l’oggetto Resource con il nostro VersionedResource, che aggiungerà il query param v con la versione della nostra applicazione:

public class VersionedResource extends ResourceWrapper implements Serializable {
   private Resource wrapped;

   public VersionedResource(Resource wrapped) {
      this.wrapped = wrapped;
   }

   @Override
   public Resource getWrapped() {
      return wrapped;
   }

   @Override
   public String getRequestPath() {
      String version = FacesContext.getCurrentInstance().
      getExternalContext().getInitParameter("myApp.VERSION");
      return super.getRequestPath() + "&amp;v=" + version;
   }

   @Override
   public String getContentType() {
      return getWrapped().getContentType();
   }

   @Override
   public String getLibraryName() {
      return getWrapped().getLibraryName();
   }

   @Override
   public String getResourceName() {
      return getWrapped().getResourceName();
   }

   @Override
   public void setContentType(String contentType) {
      getWrapped().setContentType(contentType);
   }

   @Override
   public void setLibraryName(String libraryName) {
      getWrapped().setLibraryName(libraryName);
   }

   @Override
   public void setResourceName(String resourceName) {
      getWrapped().setResourceName(resourceName);
   }

   @Override
   public String toString() {
      return getWrapped().toString();
   }
}

La versione, unica per tutte le risorse, è definita nel web.xml:

<context-param>
  <param-name>myApp.VERSION</param-name>
  <param-value>1.0</param-value>
</context-param>

Infine, è necessario registrare il ResourceHandler nel faces-config.xml:

<application>
  <resource-handler>it.cosenonjaviste.jsf.resources.VersionedResourceHandler</resource-handler>
</application>

Conclusioni

JSF 2 quindi, oltre ad una gestione centralizzata delle risorse, offre anche un sistema di versionamento legato ad esse, che comunque è possibile personalizzare o rifare a piacimento come abbiamo visto nell’ultimo esempio. Personalmente, tra i due metodi offerti da JSF, preferisco il secondo (quello del versionamento per file) perché almeno non si è costretti a “versionare” tutti i file di una libreria. Alla fine forse la soluzione ripresa da PrimeFaces è quella più semplice e più funzionale… lascio a voi la decisione!

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+