MokaByte Numero  38  - Febbraio  2000
Swing modello MVC
Questo sconosciuto
di 
Luca 
Vetti Tagliati
Come realizzare un oggetto dedicato all’acquisizione dei campi data. Metodo “setMaxLength()” della classe “JTextField”: cronaca di una funzionalità inspiegabilmente mancante.

L’obiettivo del presente articolo è illustrare le tecniche che permettono di risolvere il problema del controllo interattivo dell’input utente estendendo opportunamente le funzionalità della classe “JTextField” (API Swing). Si tratta di un problema “atavico”, ma tuttora ricorrente, presente pressoché in ogni applicativo provvisto di un’interfaccia utente, per così dire, non di “servizio”
Introduzione
La questione del controllo interattivo potrebbe essere considerata piuttosto “decrepita”; lo stesso autore ci si è imbattuto per la prima volta oltre un decennio orsono, realizzando un apposito modulo in linguaggio C (i tempi d’oro del Turbo C 2.0 della Borland). Da allora molte cose sono cambiate, ma il problema sembrerebbe essere di attualità, sebbene si ripresenti in forme diverse: chissà che ne direbbe Gianbattista Vico!
E’ possibile misurare l’incidenza del problema mediante l’analisi dell’ingente quantitativo di quesiti a riguardo presenti nel sito “www.javasoft.com”.
Il modo migliore per risolvere il caso, e qui il concetto della riusabilità trova la sua esaltazione, è quello di progettare una classe (meglio un bean), più generale possibile che, opportunamente parametrizzata, sia in grado di controllare e gestire tutte le diverse casistiche di campi dati (solo testo, numerici, monetari, data, …).
Il dominio del metodo di controllo dell’input è quindi costituito dall’insieme delle stringhe di caratteri (inserite per mezzo della funzione di “Paste”) aventi come caso più frequente, il sottoinsieme delle stringhe di lunghezza unitaria (analisi del singolo carattere premuto). In sostanza si tratta di realizzare un’estensione della classe “JTextField” che svolga il compito di “filtro-interattivo”. La classe così ottenuta, una volta ricevuta l’attenzione (“focus”) dovrebbe occuparsi di monitorare tutto ciò che le viene fornito dalla tastiera (compresi i risultati dei comandi “Copy & Paste”) al fine di accettare i dati validi e di scartare quelli non previsti dal tipo di campo. Il criterio di selezione (quali caratteri considerare validi) dovrebbe essere fornito per mezzo di un’apposita stringa di “mascheramento”.
Per esempio, se si volesse demandare alla classe il compito di acquisire i valori di un campo monetario, la si dovrebbe parametrizzare al fine di farle accettare i soli caratteri numerici, eventualmente corredati da opportuni separatori.
Per poter affrontare il problema è necessario però possedere una certa dimestichezza con il famossissimo modello “M. V. C.” (ampiamente illustrato nei numeri precedenti di MokaByte), o meglio, della versione denominata “model-delegate”, su cui si basa l’architettura Swing.
Da notare che, volenti o nolenti, è necessario affrontare il problema, dal momento che la classe “JTextField”, di per sé, non dispone di alcun metodo per limitare il numero di caratteri impostabili dall’utente.
Poiché l’esercizio, come si vedrà, non è banalissimo, ci si chiede come mai non sia stato realizzato un opportuno metodo (“setMaxLength()”) direttamente dai progettisti delle Swing. Per il lettore meno attento si vuole sottolineare che si tratta di un’interrogazione retorica.
Il problema potrebbe sembrerebbe abbastanza banale, il timore è tuttora grande, se non fosse per il fatto che anche il relativo esempio fornito nella sezione “Tips & Tricks” del sito Sun presenti un’imprecisione abbastanza rilevante: si è “semplicemente” trascurata l’architettura MVC (o meglio la relativa versione Swing)! Ci si rende conto che l’articolo voleva essere solo un esempio, ma, proprio per il suo carattere didattico e per la sua funzione di riferimento (faro per milioni di programmatori), forse sarebbe stato più opportuno porvi maggiore attenzione. L’articolo “incriminato”, attualmente, è stato rimpiazzato, ne sono rimaste tracce nell’archivio utilizzato per le ricerche (provare ad eseguire un reperimento specificando il titolo “Constructing a Masked JTextField”) e, casualmente (touché), l’autore del presente articolo (come si vedrà) ne conserva una copia in formato digitale.
 
 
 

AWT e SWING: Architetture differenti….
Lungi dal voler proporre un’ennesima (inflazionatissima) dissertazione sulla variante al modello MVC proposta nelle API Swing, per la quale esistono articoli a iosa (consultare numeri precedenti di MokaByte), in questo paragrafo, si vuole semplicemente illustrare come sia possibile realizzare un bean per l’acquisizione di un campo data, senza violare l’architettura MVC.
Si tenga presente che l’esempio fornito è ben più complesso del caso generale: non basta eseguire un controllo sintattico (quali caratteri accettare e quali rifiutare), bensì è necessario eseguire verifiche semantiche (per esempio controllo dell’anno bisestile, verifica di date inesistenti, ecc.).
Da notare che poiché l’architettura interna delle Swing, con grande giovamento dei programmatori Java, è stata completamente ristudiata, rispetto a quella malconcia e più volte rimaneggiata proposta dall’AWT, l’approccio utilizzato in precedenza per estendere i “controlli” grafici va completamente ripensato.
Se si disponeva di qualche classe già realizzata per l’ambiente AWT, si tenga presente che è tutto da rifare. Non si tratta semplicemente di recuperare (leggere “Copy&Paste”) alcune parti e scriverne altre, è necessaria una vera e propria riprogettazione: in questo caso, tutto ciò che è stato realizzato per il modello AWT è destinato al Garbage Collector (famoso concetto della riusabilità).
In base alla precedente architettura (AWT) era sufficiente definire una nuova classe, denominata (con grande impiego di risorse celebrali)  “MaskedField”, ereditante la “TextField”

public class MaskedField extends TextField
ed andare a sovrascrivere (“override”) il metodo “handleEvent”, al fine di gestire, uno per uno, i tasti premuti dall’utente.
Tali tasti, in funzione di un’opportuna stringa di mascheramento, potevano essere ignorati (valore di ritorno “false”) o accettati (valore di ritorno “true”).
public boolean handleEvent(Event oEvnt) {
Tipicamente all’interno di tale metodo si inseriva un apposito costrutto “switch” al fine di poter gestire gli eventi della tastiera desiderati, ed il gioco era pressoché fatto.
switch (oEvnt.id) {
 case Event.KEY_PRESS:
  ……
  break;
Con l’introduzione delle API Swing tale approccio non è più utilizzabile, sia perché non è possibile evitare che un tasto venga rifiutato, sia (ancor più importante) perché si contravverrebbe (almeno in linea di principio) l’architettura MVC.
Se si accede alle “FAQ Swing”, si legge che il consiglio (o meglio l’ordine) fornito è quello di agire (override) sui metodi “remove()” e “insertString()” appartenenti alla classe “PlainDocument” da associare alla classe che estende la “JTextField()” (come avviene del resto per ogni altro componente dell’”API Swing”).
protected class NewDocument extends PlainDocument {
public void remove(int iOffs, int iLen) throws BadLocationException {
  . . .
}

public void   insertString(int iOffs, String sTxt, AttributeSet oAttSet)
        throws BadLocationException {
   ...
}
}

Per associare una classe estensione di una “PlainDocuement”, è necessario inserire, all’interno della classe che specializza la “JTextField”, il seguente frammento di codice:
protected Document createDefaultModel() {
  return new NewDocument();
 }
Da qui si dovrebbe intuire, tra l’altro, perché si parla di modello per delegazione
Fin qui sembrerebbe tutto logico e lineare. Poiché si vuole specializzare il comportamento di un “oggetto” grafico e non il suo “look” è necessario estendere opportunamente la classe “PlainDocument”. La nuova tecnica risulta per molti versi semplificare il lavoro. In primo luogo, per capire se l’utente intenda cancellare o inserire una sotto-stringa, non è necessario andare a “leggere” tutti i caratteri premuti da tastiera ed intercettare la pressione del tasto “CANC” o “BACK SPACE”: è sufficiente estendere il relativo metodo; il quale, attraverso i parametri, fornisce gli indici del testo da eliminare.
Il lavoro tedioso spicca quando, invece, si deve gestire l’inserimento di un’intera stringa precedentemente memorizzata (“Copy&Paste”). La questione è così delicata che taluni programmatori, spaventati dal problema, ahimè, si riducono ad inibirne la funzionalità. Purtroppo oggigiorno non è infrequente il caso di molti programmatori che, trovandosi di fronte ad un algoritmo da realizzare, la cui complessità supera un “epsilon” piccolo a piacere, invece di affrontare il problema, decidano semplicemente di evitarlo. VB docet!
Prima di proseguire, si analizzi l’esempio fornito nell’articolo “pubblicato” nel sito “JavaSoft”.
L’obiettivo era realizzare un componente, denominato IPField, dedicato all’acquisizione di indirizzi IP di quattro byte; quartetto di sotto-stringhe numeriche di tre caratteri, separate dal carattere punto, secondo il formato visualizzato in figura 1.
 
Figura 1 “Look” del controllo “IP field”

 

La soluzione proposta è rappresentata, fedelmente, dal codice riportato di seguito.
 

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;


La classe seguente serve allo scopo di realizzare un “frame” in cui visualizzare (e provare) l’”IPfield”.

public class IPAddressDemo extends Jframe {

    public IPAddressDemo() {
        super("IP Address text field demo");
 
        JPanel panel = new JPanel();
        getContentPane().add(panel);
 
        IPField ipf = new IPField();
        ipf.setFont(new Font("Monospaced", Font.BOLD, 14));
        panel.add(ipf);
 
        setSize(250,70);
        setVisible(true);
    }
 
    public static void main(String[] args) {
        new IPAddressDemo();
    }
}
 

Classe estendente la “JTextField”.

class IPField extends JTextField {
    public IPField() {
        super();
        setText("   .   .   .   ");
        KeyAdapter ka = new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
                    int pos = getCaretPosition();
                    if (pos > 0) {
                        char[] text = getText().toCharArray();
                        text[pos-1] = ' ';
                        text[3] = '.';
                        text[7] = '.';
                        text[11] = '.';
                        switch (pos) {
                            case 2:  text[1] = text[2];
                                     text[2] = ' '; break;
                            case 6:  text[5] = text[6]; 
                                     text[6] = ' '; break;
                            case 10: text[9] = text[10]; 
                                     text[10] = ' '; break;
                            default: break;
                        }
                        setText(new String(text));
                        setCaretPosition(pos-1);
                        e.consume();
                    }
                }
                else if (e.getKeyCode() == KeyEvent.VK_DELETE) {
                    int pos = getCaretPosition();
                    if (pos < 16) {
                        char[] text = getText().toCharArray();
                        text[pos] = ' ';
                        text[3] = '.';
                        text[7] = '.';
                        text[11] = '.';
                        switch (pos) {
                            case 0: text[0] = text[1];
                            case 1: text[1] = text[2];
                            case 2: text[2] = ' '; break;
                            case 3: break;
                            case 4: text[4] = text[5];
                            case 5: text[5] = text[6];
                            case 6: text[6] = ' '; break;
                            case 7: break;
                            case 8: text[8] = text[9];
                            case 9: text[9] = text[10];
                            case 10: text[10] = ' '; break;
                            case 11: break;
                            case 12: text[12] = text[13];
                            case 13: text[13] = text[14];
                            case 14: text[14] = ' '; break;
                            case 15: break;
                            default: break;
                        }
                        setText(new String(text));
                        setCaretPosition(pos);
                        e.consume();
                    }
                }
                else if (e.getKeyChar() == '.') {
                    int pos = getCaretPosition();
                    if (pos < 4)
                    setCaretPosition(4);
                    else if (pos < 8)
                    setCaretPosition(8);
                    else if (pos < 12)
                    setCaretPosition(12);
                }
            }
        };
        addKeyListener(ka);
    }
 
    // Disable cut operations
    public void cut() {  }

    // Disable paste operations
    public void paste() { }

    protected Document createDefaultModel() {
       return new IPDocument();
    }

Classe estendente la il “plainDocument”.

    protected class IPDocument extends PlainDocument {
       public void remove() throws BadLocationException {
         super.remove(3,1);
         super.insertString(3,".",null);
         super.remove(7,1);
         super.insertString(7,".",null);
         super.remove(11,1);
         super.insertString(11,".",null);
       }
 
       public void insertString(int offset, String str, AttributeSet a)
        throws BadLocationException {
         char[] insertChars = str.toCharArray();
         boolean valid = true;
         if (insertChars.length + offset < 16) 
         {
            for (int i = 0; i < insertChars.length; i++) 
            {
                if (!(Character.isDigit(insertChars[i]) || 
                    insertChars[i] == ' ')) 
                {
                    if(insertChars[i] != '.' || 
                       ((offset + i != 3) && (offset + i != 7) && 
                        (offset + i != 11))) 
                    {
                          valid = false;
                          break;
                    }
                }
                if(insertChars[i] == '.' 
                              && getText(offset, 1).equals(".")) {
                    valid = false;
                    break;
                }
            }
         }
         else
         valid = false;
         if (valid) {
             super.insertString(offset, str, a);
             if(getLength() >= 16)
             super.remove(offset + str.length(), getLength()-15);
         }
         remove();
       }
    }
}

Sebbene la soluzione proposta funzioni egregiamente, il problema è che si sono trascurate le direttive dei progettisti delle Swing e forse anche un po’ l’architettura: si agisce fortemente sulle componenti “view” e “Controller” (che rappresentano l’”UI Delegate”) anziché sul modello.
Si tratta di un “trucchetto” (forse non così necessario) messo in atto per aggirare una serie di problemi connessi con l’inserimento di un’intera stringa (funzione di “Paste”).
 
 
 

Esempio di un oggetto dedicato all’acquisizione della data
Il codice fornito di seguito, serve a realizzare un controllo in grado di acquisire date in formato “gg/mm/aaaa”.
Si tratta di un punto di partenza; una classe “estendibile” per risolvere il problema dell’acquisizione di date in qualsivoglia formato.
L’oggetto grafico in questione è abbastanza sofisticato dacché, come già riportato, oltre ad eseguire controlli sintattici (evita che vangano inseriti caratteri alfabetici, simboli non previsti, “slash” in posizioni errate), esegue anche controlli semantici: evita che vengano impostate date inesistenti come “31/11/1999”, “29/02/2100” (ebbene il 2100 non è un anno bisestile) ecc.
La classe, così come progettata, prevede la visualizzazione di un carattere sottolineato al posto dei spazi vuoti e, come divisore di campi, uno slash; nulla vieta di variarli o di realizzare opportuni metodi (“setSepChar()” e “setEmptyChar()”) per consentire all’utente di selezionarli a proprio piacimento. 
L’aspetto del “widgets” (catturato nell’atto fatidico dell’impostazione della data della catastrofe mondiale) è illustrato in figura 2.
 
 

Figura 2 Look del controllo per l’acquisizione dei campi data.

E’ ormai chiaro che la parte di codice di interesse è quella relativa alla classe che estende la “PlainDocument”, che, visto l’argomento, è stata “battezzata” “DateDocument”.

protected class DateDocument extends PlainDocument {

    /** Array con i giorni di cui è composto ciascun mese*/
    private final int aiDaysOfMonths[] =
      {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

Il metodo “remove” (o meglio la relativa sovrascrittura) è abbastanza semplice, le uniche cose da tenere in considerazione sono:

  1. viene invocato la prima volta all’atto dell’istanziazione della classe; senza che sussista una reale necessità di eliminazione caratteri (lo stesso dicasi per il metodo “insertString()”);
  2. è necessario ricalcolare la posizione futura del cursore, dopo l’eliminazione, ogniqualvolta si verifichi che l’utente abbia eliminato un solo carattere e la posizione di default risulti coincidente a quello di uno “slash”: è inutile richiedere all’utente di inserire un carattere già presente! (effetto salto).
  public void remove(int iOffs, int iLen) throws BadLocationException {
    if (iLen > 0) {  // E’ un’operazione di eliminazione reale?
      String sInsertedDate = super.getText(0,10); //legge la data visualizzata
      // determina la stringa corredata dalla maschera 
      String sNewDate = replaceStr(sInsertedDate, iOffs, getSignChar(iOffs,iLen));
        super.remove(0,10); //..elimina tutto il testo visualizzato
        super.insertString(0,sNewDate,null); //..Inserisce quello risultante

      // Determina la nuova posizione
      if (iLen == 1) {      // Si è eliminato un singolo carattere?

        if ((iOffs == 3) || (iOffs == 6))  // Ci si trova su uno slash?
          iOffs--;  //..arretra la posizione del cursore
      }

      setCaretPosition(iOffs);   // posizione il cursore
    }
  }

Si noti che si sono invocati  metodi “remove()” e “InsertString” della classe padre al fine di evitare chiamate ricorsive che, tra l’altro, genererebbero un “dead lock”.

Il metodo “insertString()” è decisamente più complesso di quello precedente, e svolge i seguenti di compiti:

  1. Filtra la stringa che si vuole inserire (elimina i caratteri non numerici);
  2. Esegue il controllo semantico dei caratteri numerici. Qualora individui un dato errato (per esempio 31/11), lo rimpiazza con un carattere sottolineato (3_/11) e quindi vi si posiziona con il cursore;
  3. Calcola la nuova posizione del cursore.
public void   insertString(int iOffs, String sTxt, AttributeSet oAttSet)
       throws BadLocationException {

  // Si tratta di un reale nuovo inserimento?
  if ((sTxt != null) && (sTxt.length() > 0)) {

    String sTxtInserted = this.getText(0,10);  // preleva la stringa video

    //--------------------------------------------------
    //Verifica se c’è spazio per l’inserimento di un altro carattere

    if (iOffs > iDEF_LENG_INT+1) {  //Si sta cercando di andare oltre la fine?

      int iFirstEmpty = sTxtInserted.indexOf(getMaskChar());

      if (iFirstEmpty > -1) //E’ presente almeno uno spazio vuoto?
          setCaretPosition(iFirstEmpty); //Si, posiziona il cursore sul primo.

      beep();

      return;
    }

    // -----------------------------------------------------------------
    // Elimina gli eventuali caratteri errati presenti nella stringa

    int iIntStrLen = sTxt.length();  // lunghezza della stringa da inserire

    sTxt = getOnlyDigit(sTxt);       // Elimina eventuali caratteri non validi

    if (iIntStrLen != sTxt.length()) { // Eliminati caratteri non validi?
      beep();
    }

    // --------------------------------------------------------------
    // Verifica se l’utente stia tentando di inserire troppi caratteri

    iIntStrLen = getOnlyDigit(sTxtInserted).length();

    iIntStrLen += sTxt.length(); //Lunghezza della data inclusa la nuova stringa

    // troppi caratteri?
    if ((iOffs - getSlashNumber(iOffs))+sTxt.length() > iDEF_LENG_INT) {
      beep();
      // Elimina quelli in eccedenza
      sTxt = sTxt.substring(0, iDEF_LENG_INT + getSlashNumber(iOffs) - iOffs);
    }

    // --------------------------------------------------------------------
    // Aggiunge la stringa da inserire 

    if (sTxt.length() > 0) {

      //Cio’ che si vuole inserire “sovrascrive” i caratteri slash?
      if ( (iOffs < 2) && (sTxt.length()+iOffs > 2)) {

        sTxt = insertSubstr(sTxt, (2-iOffs), getSeparChar()+"" ); }

        if(iOffs == 2) //Il nuovo inserimento è sul primo slash?
          iOffs++; //..si => si sposta in avanti

        if ((iOffs < 5) && (sTxt.length()+iOffs > 5)) { //comprende il 2 slash?
            sTxt = insertSubstr(sTxt, (5-iOffs), getSeparChar()+"");  //Riformatta la stringa con i caratteri di controllo
        }

        if(iOffs == 5) //Si vuole inserire sopra il secondo slash?
          iOffs++;   //si => lo salta.
      }

      // --------------------------------------------------------
      // Esegue i controlli semantici sulla data

      // prepara la nuova stringa da inserire
      sTxtInserted = replaceStr(sTxtInserted, iOffs, sTxt);  //aggiunge il testo

      sTxtInserted = getValidCharacters(sTxtInserted); // Verifica se la data è valida

      // --------------------------------------------------------
      // Mostra la nuova stringa

      super.remove(0,10); //Elimina la stringa visualizzata
      super.insertString(0,sTxtInserted,oAttSet);//.Inserisce quella nuova

      // ----------------------------------------------------------
      // Calcola la nuova posizione del cursore

      setCaretPosition(getNextAvailablePos(iOffs,sTxtInserted,sTxt.length()>0));

    } else {                                         //Operazione fittizia?

      super.insertString(0,getSignChar(0,10),oAttSet);//Visualizza la formattazione
    }
  }

  /** Determina la posizione del primo “carattere” vuoto (‘_’) nella stringa
    *
    * @param iCurrPos Posizione attuale del carset
    * @param sDate    Data visualizzata
    * @param boInsStr Indica se si sta eseguendo il metodo di inserimento o meno
    * @return iNewPos Posizione del primo spazio disponibile per l’inserimento
    */

  private int getNextAvailablePos(int iCurrPos, String sDate, boolean boInsStr) {

    int iNewPos = sDate.indexOf(getMaskChar());

    if (iNewPos == -1) {   // Nessun spazio vutoto?

      if (boInsStr) {      // Si sta eseguendo un inserimento?

        iNewPos = 10;      // Posiziona il cursore oltre la fine della stringa

      } else {

        iNewPos = iCurrPos;

        if (iNewPos < 10) {// Non si è ancora raggiunta la fine della stringa?
          iNewPos++;       // … Si posiziona al prossimo carattere

          if ( (iNewPos == 2) || (iNewPos == 5))  // Ci si trova su uno slash?
            iNewPos++;                            // … Avanza di una posizione
        }
      }
    }

    return iNewPos;
  }

  /** Verifica che il carattere fornito sia compreso nei limiti specificati
    *
    * @param cToVer   Carattere da verificare
    * @param cValMin  Limite inferiore
    * @param cValMax  Limite superiore
    * @return True se il carattere fornito appartiene all’intervallo specificato
    */

  private boolean verCharValidity(char cToVer, char cValMin, char cValMax) {

    boolean boRet = true;

    if (cToVer != getMaskChar()) { //Non si tratta di un carattere di formattazione?

      if ((cToVer < cValMin) || (cToVer > cValMax)) {
        boRet = false;
      }

    }

    return boRet;
  }

  /** Elimina i caratteri non validi presenti nella stringa fornita
    *
    * @param sText Stringa da verificare
    * @return      Stringa con i soli caratteri validi
    */

  private String getValidCharacters(String sText) {

    String sValidate = "";
    boolean boErr = false;
 

    // -------------------------------------------- Carattere in pos. 0
    if (!verCharValidity(sText.charAt(0), '1','3')) {
      boErr = true;
      sValidate += getMaskChar();
    } else {
      sValidate += sText.charAt(0);
    }

    // -------------------------------------------- Carattere in pos. 1
    if (sText.charAt(0) == '3'){

      if(!verCharValidity(sText.charAt(1), '0','1')){
        boErr = true;
        sValidate += getMaskChar();
      } else {
        sValidate += sText.charAt(1);
      }

    } else {
      sValidate +=sText.charAt(1);
    }

    // ------------------------------------------- Carattere in pos. 2 ==> slash
    sValidate += sText.charAt(2);

    // ------------------------------------------- Carattere in pos. 3
    if (!verCharValidity(sText.charAt(3), '0','1')) {
      boErr = true;
      sValidate += getMaskChar();
    } else {
      sValidate += sText.charAt(3);
    }

    // ------------------------------------------- Carattere in pos. 4
    if (sText.charAt(3) == '1'){
      if(!verCharValidity(sText.charAt(4), '0','2')){
      boErr = true;
      sValidate += getMaskChar();
      } else {
        sValidate += sText.charAt(4);
      }
    } else {
      sValidate +=sText.charAt(4);
    }

    // ------------------------------------------- Carattere in pos. 5 ==> slash
    sValidate += sText.charAt(5);
 

    // -------------------------------------------- Carattere in pos. 6
    if (!verCharValidity(sText.charAt(6), '1','2')) {
      boErr = true;
      sValidate += getMaskChar();
    } else {
      sValidate += sText.charAt(6);
    }

    // ----------------------------------- Caratteri dalla pos. 7 a fine stringa
    sValidate = sValidate + sText.substring(sValidate.length(),10);

    // --------------------------------------------------------------------
    // Controlli semantici

    String sDays  = sValidate.substring(0, 2);   // Giorno
    String sMonth = sValidate.substring(3, 5);   // mese
    String sYear  = sValidate.substring(6,10);   // anno

    int iMonthId = -1;

    // Presenti sia il mese sia l’anno?
    if ((getOnlyDigit(sMonth).length() == 2) && (getOnlyDigit(sDays).length() == 2)) {

      iMonthId       = Integer.parseInt(sMonth)-1;
      int iNumOfDays = aiDaysOfMonths[iMonthId];  // preleva il num. di giorni presenti nel mese impostati

      if (iNumOfDays < Integer.parseInt(sDays)) { // giorno errato?
        boErr = true;
        sValidate = replaceStr(sValidate,1,getMaskChar()+"");
      }

      // Verifica dell’anno bisestile
      // Si è preferito affidare il controllo al JDK API

      // Si sta considerando il giorno 29 del mese di Febbraio?
      if ((iMonthId == 1) && (Integer.parseInt(sDays)) == 29) {

        // E’ presente il campo anno?
        if ((!boErr) && (getOnlyDigit(sYear).length() == 4)) {

          int iYear = Integer.parseInt(sYear);

          GregorianCalendar oCal = new GregorianCalendar();

          if (!oCal.isLeapYear(iYear)) {
            boErr = true;
            sValidate = replaceStr(sValidate,1,getMaskChar()+"");
          }
        }
      }
    }

    if (boErr) {  // Riscontrato un errore?
      beep();
    }

    return sValidate; 
  }

  /** Sostituisce una sottostringa in un dato oggetto stringa
    *
    * @param sSource Stringa da aggiornare
    * @param iStrPos Indice in cui iniziare la sostituzione
    * @param sToRep  Sottostringa da sostituire
    * @return        Stringa risultante
    */

  private String replaceStr(String sSource, int iSrtPos, String sToRep) {

    String sBefore = sSource.substring(0,iSrtPos);// Sottostringa che precede il punto di sostituzione
    String sAfter  = sSource.substring(iSrtPos+sToRep.length());//.Sottostringa che segue il punto di sostituzione

    String sRet = sBefore+sToRep+sAfter;// Nuova stringa

    return sRet;
  }
 

  /** Inserisce una sottostringa in un dato oggetto stringa
    *
    * @param sSource Stringa da aggiornare
    * @param iStrPos Indice in cui iniziare l’inserimento
    * @param sToRep  Sottostringa da inserire
    * @return        Stringa risultante
    */

  private String insertSubstr(String sSource, int iSrtPos, String sToIns) {

    String sBefore = sSource.substring(0,iSrtPos); // Sottostringa che precede il punto di inserimento
    String sAfter  = sSource.substring(iSrtPos);   //. Sottostringa che segue il punto di inserimento
    String sRet = sBefore+sToIns+sAfter; // nuova stringa

    return sRet;
  }

  /** It reurns a string of significant characters
    * in the right positions
    *
    * @param iOffs is the starting point in the date string of the substring to be elaborate
    * @param iLen is the length of this substring
    * @return an sValidate only digit string
    */

  private String getSignChar(int iOffs,int iLen){
    String sSignChar = "";

    for (int iIncr = 0; iIncr< iLen; iIncr ++){ //let's work on this string

    if (iOffs == 2 || iOffs == 5)   //are we on the separator position
      sSignChar += getSeparChar();  //let's put it
    else
      sSignChar +=  getMaskChar();  //and here let's put mask char for our date field!
      iOffs++;
    }

    return sSignChar; //the right separator and masked
                      //chars for the substrig position
  }
 

  /** Preleva dalla stringa data i soli campi numerici (elimina i caratteri di formattazione)
    *
    * @param sText  Stringa contenente il campo data
    * @return       Data filtrata dei caratteri di formattazione 
    */

  private String getOnlyDigit(String sText){

    int iInd = 0;  // Contatore di ciclo
    char cCurr;    // Carattere “oggetto” del singolo ciclo
    String sValidate = "";

    while (iInd < sText.length()) {
      cCurr = sText.charAt(iInd);  //preleva il carattere in posizione “iInd”
      if (Character.isDigit(cCurr))
        sValidate += cCurr ;
        iInd++; // Prossimo carattere

      }
    return sValidate;
  }

/** Determina il numero di caratteri slash che precedono la posizione specificata
  *
  * @param iOffs Posizione da considerare
  * @return      Numero di slash che precedono la posizione in questione
   */

  private int getSlashNumber (int iOffs){
   int iWithSlash = 0;

    if (iOffs > 2) //Passata la seconda posizione?
     iWithSlash++; //..Si consideri il primo slash.
    if (iOffs >5)  //Passata la quinta posizione?
     iWithSlash++; //.. Si consideri anche il secondo slash.
    return  iWithSlash;
  }
}
}

Conclusioni
Il presente articolo è scaturito da un’esigenza pratica: realizzare un “widgets” dedicato al controllo dei dati impostati dall’utente. L’idea è scaturita a seguito delle numerose e continue richieste di delucidazione riguardanti l’argomento.
Come si è potuto osservare, la progettazione delle API Swing ha determinato, come logica conseguenza, la necessità di ristudiare i controlli grafici basati sulle API AWT, al fine di adeguarli alla nuova struttura interna: non si tratta di un “cut&Copy”, bensì di una reingegnerizzazione vera e propria.
Sebbene l’esempio fornito, controllo di un campo data, risolva un sottocaso specifico, date in formato <gg>/<mm>/<aaaa>, si è convinti che si tratta di una buona base di partenza per la realizzazione di una classe più flessibile, in grado di acquisire date nei formati più vari.

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it