giovedì 23 maggio 2013

Stampare PDF da AjaxLink

Chi usa Wicket con Internet Explorer 7 o 8 potrà essere incappato nel problema del caching dei link. Questo genera varie problematiche, una delle quali riguarda gli applicativi che permettono di stampare file PDF alla pressione di un link.
Il sistema di cache tende infatti a non far lanciare a Wicket la onClick del link con la conseguenza che eventuali variazioni sui dati avvenuti in seguito, per esempio, ad un filtro su una form, non venissero recepiti dalla funzione di creazione PDF che stampava quindi i vecchi risultati.

La soluzione in questione permette di introdurre il concetto di stampa PDF anche da AjaxLink. Questo risolve la problematica di cui sopra perché l'Ajax Link non viene riconosciuto come componente analogo e quindi non entra nella cache.

Per realizzare questo, basta creare la seguente classe:



import java.io.InputStream;

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;

public class AJAXFileDownload extends AbstractAjaxBehavior
{
 private static final long serialVersionUID = 1L;
 private boolean addAntiCache;
 private String fileName;
 private InputStream is;

    public AJAXFileDownload() {
     this(true);
 }
     
 public AJAXFileDownload(boolean addAntiCache) {
     super();
     this.addAntiCache = addAntiCache;
 }

 /**
  * Call this method to initiate the download.
  */
  public void initiate(AjaxRequestTarget target, String fileName, InputStream is) { 
         this.fileName = fileName; 
         this.is = is; 
         String url = getCallbackUrl().toString(); 

         if (addAntiCache) { 
             url = url + (url.contains("?") ? "&" : "?"); 
             url = url + "antiCache=" + System.currentTimeMillis(); 
         } 

         // the timeout is needed to let Wicket release the channel 
         target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);"); 
     } 

 public void onRequest() {
  ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(getResourceStream(), getFileName());
  handler.setContentDisposition(ContentDisposition.ATTACHMENT);
  getComponent().getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
 }
 
 protected String getFileName() {
  return this.fileName;
 }

 /**
  * Hook method providing the actual resource stream.
  */
    protected IResourceStream getResourceStream() { 
        return new AbstractResourceStreamWriter() { 
   private static final long serialVersionUID = 1L;
   public void write(Response output) { 
                WebResponse response = (WebResponse)output; 
                byte[] buffer = new byte[256]; 
                try { 

                    int read; 
                    while((read = is.read(buffer)) != -1) { 
                        response.write(buffer, 0, read); 
                        response.flush(); 
                    } 
                    response.close(); 

                } catch (Exception e) { 
                    response.write(e.toString().getBytes()); 
                    response.close(); 
                } 
            } 
        }; 
    } 
}
Usarla è molto semplice.
//Istazione l'oggetto di cui sopra
final AJAXFileDownload fileDownload = new AJAXFileDownload(); 

AjaxLink printLink = new AjaxLink("stampa"){
   public void onClick(AjaxRequestTarget target) {
      
       //operazioni per avere il contenuto del PDF
       final byte[] pdf = creaPDFinQualcheModo();

       fileDownload.initiate(target, "nomefile.pdf", new ByteArrayInputStream(pdf));
   }
};

printLink.add(fileDownload);

add(printLink); //aggiungo il link al componente padre
In questo modo potete generare tutti i PDF che volete.

martedì 10 luglio 2012

Wicket - Property Column con variazione valore

Diciamo che avete un Property Column legato ad una proprietà di un POJO che viene riempito da DB. Nella  tabella però volete manipolare la resa visiva modificando quindi il valore dentro la proprietà.

Per esempio state stampando un elenco di messaggi e volete stampare solo i primi 100 caratteri del corpo che altrimenti sarebbe troppo lungo.

Per farlo occorre sovrascrivere il metodo PopulateItem della property column come segue:

PropertyColumn colonnaCorpo = new PropertyColumn<Messaggio>(new Model<String>("Corpo"), "corpo"){
    @Override
    public void populateItem(Item<ICellPopulator<Messaggio>> item,
            String componentId, IModel<Messaggio> rowModel) {
        String corpoModel = rowModel.getObject().getCorpo();
        item.add(new Label(componentId,corpoModel.substring(0, Math.min(50,corpoModel.length()))));
    }                
};

Il nostro POJO di partenza si chiama Messaggio e ha dentro una proprietà stringa chiamata corpo.
Sovrascrivendo la populateItem, andiamo a sovrascrivere il modo in cui il componente viene stampato.

item: rappresenta l'oggetto colonna;
componentId: rappresenta l'id del singolo componente (in questo caso dinamico dato che è una cella di una tabella);
rowModel: rappresenta il modello del POJO di cui questo componente sta per stampare una proprietà;

Nell'esempio recuperiamo il testo del corpo dal modello richiamando prima l'oggetto Messaggio dal Modello e quindi richiamando la getCorpo().

A questo punto aggiungo alla item, una Label con lo stesso ID e come stringa la sottostringa del corpo facendo in modo che Java non si incavoli se chiedo più caratteri di quanto la stringa sia effettivamente lunga (leggasi, aggiungo il MIN).

Questo barbatrucco è molto comodo quando dovete ridefinire il modo in cui il componente stampa il valore. Le potenzialità sono multiple. Potete per esempio inserire un TextField dentro la cella, o un Panel con dentro altra roba ecc...

mercoledì 4 luglio 2012

Wicket - Input Text con Autocomplete

Prima o poi ti chiedono anche di fare un input text con i suggerimenti alla Google. Wicket ci permette di farlo in pochi passi.

final AutoCompleteTextField destinatario = new AutoCompleteTextField("destinatario", new PropertyModel<String>(email,"userNameDestinatario"))
 {
      protected Iterator getChoices(String input)
      {
          if (Strings.isEmpty(input))
          {
              return Collections.EMPTY_LIST.iterator();
          }
          
          List<String> elencoFittizio = new ArrayList<String>();
          elencoFittizio.add("Pippo");
          elencoFittizio.add("Pluto");
          elencoFittizio.add("Paperino");
          elencoFittizio.add("Minnie");
          elencoFittizio.add("Paperone");
          elencoFittizio.add("Nonna Papera");
          
          List<String> elencoPulito = new ArrayList<String>();
          
          for (String nome : elencoFittizio){
              nome = nome.toLowerCase();
              String probe = input.toLowerCase();
              if (nome.contains(probe)){
                  elencoPulito.add(nome);
              }
          }
          return  elencoPulito.iterator();
       }
 };

Per farlo usiamo il componente AutoCompleteTextField che aggancieremo ad un tag HTML classico di tipo INPUT TEXT.

Come vedete il costruttore è piuttosto classico. Si definisce il wicket:id (destinatario) e nel mio caso il PropertyModel così da riempire direttamente la proprietà userNameDestinatario dentro l'oggetto email.

A questo punto occorre implementare il metodo che gestisce cosa farci vedere. Come vedete ci viene dato in ingresso una stringa input che rappresenta ciò che l'utente sta inserendo.

Il metodo richiede in uscita un Iterator ovvero l'oggetto che permette a Java di scorrere le Collections ovvero qualsiasi oggetti che rappresenta un elenco di qualcosa.

Se la stringa è vuota, creo una Collections vuota e ritorno l'iterator.

Negli altri casi, devo ritornare l'iterator della lista pulita ovvero della lista risultante dal controllo con l'input inserito.

Nel codice sopra ho mokkato il codice inserendo una lista finta. E' ipotizzabile che qui agganciate un metodo che vada su DB a fare la query e ritorni una lista di cui poi ritornerete l'iterator.

Per farvi capire bene però, nell'esempio ho una lista completa che poi vado a sfoltire sulla base della stringa in ingresso. Nel caso sopra creo una nuova lista con gli elementi della prima che contengono la stringa in input.

Se invece avessi voluto l'aggancio ad un eventuale DB tramite un manager, avrei avuto:


final AutoCompleteTextField destinatario = new AutoCompleteTextField("destinatario", new PropertyModel<String>(email,"userNameDestinatario"))
 {
      protected Iterator getChoices(String input)
      {
          if (Strings.isEmpty(input))
          {
              return Collections.EMPTY_LIST.iterator();
          }
          
          List<String> utentiFiltrati = getManager().cercaUtentiLike(input);
          return  utentiFiltrati.iterator();
       }
 };

Come vedete non mi interessa cosa faccia la cercaUtentiLike, mi interessa solo che mi ritorni una Lista di Stringhe, probabilmente filtrate in fase di interrogazione al DB.

martedì 3 luglio 2012

Wicket - Triggerare il feedback Panel di una Pagina indipendentemente dalla profondità

Un titolo lungo per qualcosa di assolutamente semplice ma potente.

Avevo una situazione di questo tipo:

Pagina con dentro un Panel con dentro una Modale con dentro un Panel. Premendo un tasto di SAVE, dovevo chiudere la modale e mostrare nel feedback panel della pagina, un messaggio di avvertimento all'utente che l'operazione era andata a buon fine.

Per farlo serve un metodo getFeedBackPanel() da dare alla Page e che ritorna l'oggetto.
A questo punto usiamo questo codice:

FeedbackPanel feedbackPadre = ((ProtectedPage) findPage()).getFeedBackPanel();
feedbackPadre.info("Scrivo qualcosa");
target.addComponent(feedbackPadre);

Come vediamo, uso findPage() per trovare un oggetto di tipo Page  (o suo figli) a ritroso nella gerarchia indipendentemente dalla profondità. La devo ovviamente castare alla pagina effettiva che mi aspetto (nel mio caso ProtectedPage) e quindi richiamare il nostro FeedbackPanel.

Poi in maniera classica, ci scrivo qualcosa dentro e quindi lo aggancio all'evento Ajax.

venerdì 8 giugno 2012

Wicket - Modale con FORM

Nell'altro posto abbiamo costruito una modale con un contenuto statico. Purtroppo è una eventualità più remota rispetto ad una modale con dentro del codice da eseguire come nel caso di una FORM.

Il problema delle form dentro le modali riguarda il fatto che a tutti gli effetti tale finestra è una parte della pagina stessa quindi se inserite nella modale una form classica con un submit classico, alla pressione verrà caricata tutta la pagina facendo così scomparire la modale.

Per questo motivo tutte le componenti dentro la modale attiva devono essere sviluppate tramite Ajax. Fortunatamente Wicket ci da una mano.

Costruire il Panel


Il contenuto della modale viene realizzato tramite un pannello che funziona esattamente come quelli che generalmente realizzate. L'unica variazione riguarda la variabile della Finestra Modale che va passata al pannello così che una delle operazioni al suo interno possa chiuderla (il submit con successo per esempio).

final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
public EditMicrorganismiLatteCrudo(String idPojo, final ModalWindow modalWindow) {
   super("content");
   add(new MyForm(idPojo,modalWindow));
}

Il costruttore del pannello è molto semplice. In ingresso abbiamo un qualsivoglia id legato ad un POJO a cui legherò la form (giusto per fare qualcosa) e la modale. Credo e aggiungo la form che sarà espressa tramite classe privata ed a cui passo di nuovo la modale.

IMP: Il feedback del pannello va esplicitato sia nel codice HTML che all'interno della classe.

add(new AjaxButton("Salva") {
    private static final long serialVersionUID = 1L;

    @Override
    protected void onSubmit(AjaxRequestTarget target, Form<?> form) {        
        getManager().inserisciPojo(myPojo);
        modalWindow.close(target);
    }

    @Override
    protected void onError(AjaxRequestTarget target, Form<?> form) {
        target.addComponent(feedbackPanel);
    }

});

Come anticipato, gli eventi devono essere tutti Ajax quindi il bottone di submit deve necessariamente esserlo. La onSubmit funziona analogamente ad un comune tasto submit con la differenza che se va a buon fine, si deve chiudere la modale.

L'altra cosa importante riguarda il feedback. In caso di errore occorre esplicitare il comportamento essendo tutto in Ajax. La form ha ovviamente un validatore interno in base a come avete impostato i campi. Se per esempio avete inserito un TextField<Integer> e poi inserite una stringa, la FORM riconosce l'errore e alla pressione del tasto salva, lancerà il metodo onError. A questo punto non vi resta che aggiornare il feedback panel con il solito comando target.addComponent.

IMP: Il feedback deve essere aggiunto all'interno della form in quanto è un elemento figlio dello stesso.

giovedì 31 maggio 2012

Wicket - Usiamo il DataGrid

Altra richiesta ed altro componente da usare.
Mettiamo che avete una bella form con tanti input e poi vi chiedano di metterli su due o più colonne.

Per gestire questo genere di problematiche, andiamo ad usare il GridView che permette di specificare come riempire una singola cella della griglia per poi specificare il numero di righe o di colonne.

GridView<MyFormItem> gridView = new GridView<MyFormItem>("rows",new ExternalDataProvider<MyFormItem>(elencoDellaRoba))
{
    private static final long serialVersionUID = 1L;
    @Override
    protected void populateItem(Item<MyFormItem> item)
    {
        final MyFormItem r = item.getModelObject();
    final TextField<Integer> na = new TextField<Integer>("braccia", new PropertyModel<Integer>(r, "numeroDiBraccia"));
    item.add(na.setRequired(true));
    item.add(new Label("bracciaLabel",r.getDescrizione()));
    }

    @Override
    protected void populateEmptyItem(Item<Rpp_LatteCrudo> item)
    {
        item.add(new Label("braccia", "").setVisible(false));
        item.add(new Label("bracciaLabel", "").setVisible(false));
    }
};
gridView.setColumns(2);
add(gridView);

Il GridView è facile da usare analogamente al DataView che abbiamo visto più volte. L'unica parte importante riguarda il metodo populateEmptyItem che serve per decidere cosa stampare nel caso la lista non sia interamente divisibile per il numero di colonne che decideremo. Nel nostro caso, avendo scelto due colonne, se ho una lista dispari l'ultima cella sarà riempita con quello indicato in questo metodo.

Nel mio caso stampo due Label vuote e le nascondo.

Andiamo a vedere il codice HTML a cui attaccare questa cosa:

<table cellspacing="0" cellpadding="2" border="1">
    <tr wicket:id="rows">
        <td wicket:id="cols">
         <ul>
             <li><b><span wicket:id="bracciaLabel"></span></b></li>
             <li><input type="text" size="2" maxlength="3" wicket:id="braccia" /></li>
         </ul>
    </td>
    </tr>
</table>

Come vedete occorre solo definire una struttura che lui ripeterà analogamente a quanto succede per il DataView.
L'uso della tabella, in questo caso, è puramente opzionale. L'importante è definire un componente di tipo ROWS ed uno di tipo COLS. Potete mettere degli UL, degli SPAN ecc... e gestirvi tutto tramite CSS. La tabella a mio parere è più facile da gestire e più vicina al concetto di GRIGLIA.

All'interno del "COLS", definiamo cosa e come stampare la roba ricordandoci che è la stessa che dovremo mettere nella populate item.

Anche per questo componente avrete a disposizione il navigator.

martedì 29 maggio 2012

Wicket - Sostituire un Panel da una chiamata Ajax

Come sappiamo i Panel sono l'unità di sviluppo primaria in Wicket e vengono aggiunti alle pagine per generare i contenuti.
I Panel sono molto comodi perché, se fatti bene, sono altamente riutilizzabili.

In questo articolo vediamo velocemente come aggiornare il contenuto di un Panel, legato ad un POJO, in seguito ad un evento AJAX.

SITUAZIONE


Avete una pagina con una form che fa una qualche ricerca e volete stampare l'oggetto che vi ritorna in una scheda (non tabella) sotto la form stessa.

COME PROCEDERE

Per prima cosa create un Pannello (che chiameremo SchedaPanel) ed aggiungetela alla pagina assieme alla form.

Il codice del Panel deve avere la parte della scheda dati nascosta se il POJO è nullo altrimenti prima ancora di submittare la form, vedrete la scheda. Per farlo ci viene in contro l'utile accrocchio di Wicket chiamato ENCLOSURE.

<wicket:panel>
    <fieldset class="informazioni info-ditta" wicket:enclosure="dettagli">
        <legend>Dettagli </legend>
        <div wicket:id="dettagli">
            <ul>
                <li><b>Denominazione</b>: <span wicket:id="denominazione"></span></li>
                <li><b>Forma Giuridica</b>: <span wicket:id="formaGiuridica"></span></li>
                <li><b>Codice Fiscale o PIVA</b>: <span wicket:id="partitaIva"></span></li>
                <li><b>Ragione Sociale</b>: <span wicket:id="codiceFiscale"></span></li>
                <li><b>Indirizzo</b>: <span wicket:id="toponimo"></span> <span wicket:id="via"></span> <span wicket:id="civico"></span> <span wicket:id="cap"> </span><span wicket:id="comune"></span> <span wicket:id="provincia"></span></li>
                <li><b>Telefono</b>: <span wicket:id="telefono"></span></li>
                <li><b>Email Certificata</b>: <span wicket:id="emailCertificata"></span></li>
            </ul>            
        </div>
    </fieldset>
</wicket:panel>

Il codice HTML è molto semplice. Abbiamo un elenco di wicket:id nella quale andremo a mettere i dati del nostro POJO.

Notate però come il FIELDSET sia associato ad un ENCLOSURE chiamato dettagli che guarda caso è uno degli id wicket interni (il DIV).
Le enclosure servono per nascondere l'intero blocco se l'elemento figlio associato è nascosto.

In questo caso quindi se l'oggetto associato al wicket ID dettaglio ha setVisibile a false, allora tutto il fieldset sarà nascosto. E' utile per evitare di dover settare a false tante cose. In pratica il div comanda l'intera visione del blocco.

@Override
protected void onInitialize() {
    super.onInitialize();
    
    dettagli = new WebMarkupContainer("dettagli");
    dettagli.setVisible(false);
    add(dettagli);
    buildSchedaDitta();
}

La initialize del pannello è molto semplice e come vediamo mettiamo a false il div dettagli così da triggerare l'Enclosure.

public void buildSchedaDitta(){
    dettagli.setVisible(true);
    if (this.ditta == null) {
        ditta = new DittaParix();
        dettagli.setVisible(false);
    }
    else {
        dettagli.add(new Label("denominazione",ditta.getDenominazione()));
        dettagli.add(new Label("partitaIva",ditta.getPartitaIva()));
        dettagli.add(new Label("codiceFiscale",ditta.getCodiceFiscale());
        dettagli.add(new Label("formaGiuridica",ditta.getFormaGiuridica()));
        dettagli.add(new Label("toponimo",ditta.getToponimo()));
        dettagli.add(new Label("via",ditta.getVia()));
        dettagli.add(new Label("cap",ditta.getCap()));
        dettagli.add(new Label("civico",ditta.getCivico()));
        dettagli.add(new Label("telefono",ditta.getTelefono()));
        dettagli.add(new Label("emailCertificata",ditta.getEmailCertificata()));
        dettagli.add(new Label("provincia",ditta.getProvincia()));
        dettagli.add(new Label("comune",ditta.getComune()));
        
    }
}

Il metodo di costruzione della scheda è anch'esso intuitivo. Controllo se mi è stata settata la ditta nel qual caso ne inizializzo una a vuota e rimetto a false la visibilità del pannello.
Se invece qualcuno mi ha settata la ditta, questo metodo metterà a true la visibilità e riempirà i dati con le proprietà del POJO.

@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
    SchedaPanel panelTemporaneo = new SchedaPanel("schedaPanel");
    try {
        ditta = QUALCHE_METODO(PIVA);
        if (ditta != null){ 
           panelTemporaneo.setDitta(ditta);
        }
    }
    catch (Exception e){
        fatal("ERRORE: Si è verificato un errore di comunicazione oppure la ditta non è stata trovata");
        target.addComponent(feedbackPanel);
    }
    panelTemporaneo.setOutputMarkupId(true);
    panelOriginale.replaceWith(panelTemporaneo); 
    panelOriginale = panelTemporaneo;
    target.addComponent(panelOriginale);
    target.addComponent(feedbackPanel);
}

La form invece che un Button normale, avrà un AjaxButton perché non dobbiamo riaggiornare tutta la pagina ma solo il panel.
Nella onSubmit lanceremo il nostro metodo di ricerca, riempiremo il nostro POJO ditta e creeremo un nuovo Pannello settandogli la ditta (innescando così la setVisible a true di cui sopra).

La parte interessante arriva adesso. panelOriginale, che era il panel dello stesso tipo aggiunto alla pagina assieme alla form, viene rimpiazzato dalla nuova istanza aggiornata con i dati del Pojo tramite la replaceWith.

A questo punto non dobbiamo fare altro che dire a Wicket di aggiornare sia il panel originale che il feedbackpanel (nel caso vi siano eccezioni) tramite il consueto target.addCompoment.