MokaByte
Numero 21 - Luglio 1998
|
|||
|
|
||
Daniela Ruggeri |
|
||
In questo articolo analizziamo il pacchetto InfoBus che descrive la comunicazione tra classi appartenenti alla stessa JVM.
Introduzione
Architettura
del flusso di dati tra i componenti
Tipi
di componenti
Principali
caratteristiche di InfoBus
Il
protocollo InfoBus per lo scambio dei dati
Descrizione
package javax.infobus
Descrizione
esempio TimeSource-Clock
Queste specifiche forniscono standards tramite i quali un grande numero di componenti Java possono scambiarsi dati (dai produttori dei dati ai consumatori). L'insieme di interfacce che realizzano questa operazione e che i componenti devono implementare prende il nome di InfoBus.
L'architettura InfoBus facilita la creazione di applicazioni costruite su Java Beans che si scambiano dati asincronamente. Questo può essere realizzato anche sia dalle applets presenti in una pagina HTML che da beans assemblati da un generatore di applicazioni java. InfoBus può anche essere usato da qualsiasi altra classe come per esempio le servlets.
Questa prima
versione 1.1. InfoBus supporta il JDK 1.1. ma può essere
usata con il JDK 1.2, anche se non sfrutta le nuove caratteristiche del
JDK 1.2.
Architettura del flusso di dati tra i componenti.
InfoBus è stato progettato per componenti che lavorano nella stessa Java Virtual Machine (JVM).
In linea generale tutti i beans caricati da un singolo class loader possono vedere altri beans dello stesso loader e costruire metodi che realizzano chiamate dirette a quei beans.
I beans in genere usano l'introspezione per imparare o scoprire informazioni circa le classi peer dei beans al momento del run time. In tale caso un bean può dedurre un'API supportata dall'altro bean dall'analisi dell'insieme dei nomi dei metodi scoperti durante l'introspezione. Invece con le interfacce InfoBus nessuna deduzione è richiesta e le procedure possono essere chiamate direttamente perché esiste il mediatore InfoBus.
Le interfacce
di InfoBus permettono al disegnatore delle applicazioni di creare
flussi di dati tra i beans cooperanti. Al contrario del modello di risposta
ad eventi, dove la semantica dell'interazione dipende dalla comprensione
dello specifico evento collegato al bean e dalla chiamata dello specifico
metodo supportato da quell'evento, le interfacce InfoBus hanno pochissimi
eventi con un invariante insieme di metodi uguali per tutti i componenti.
La semantica del flusso di dati è basata sull'interpretazione del
contenuto dei dati che passa attraverso queste interfacce, e non dal nome
o dai parametri degli eventi e neanche dal nome e dai parametri dei metodi
richiamati.
I Beans nelle
applicazioni InfoBus possono essere di tre tipi: produttori (data
producers), consumatori (data consumers) e controllori (data
controllers). Un componente può essere sia produttore che consumatore.
I flussi di dati visti come oggetti prendono il nome di elementi (data
items). I controllori sono componenti specializzati che fungono da
mediatori nell'appuntamento tra produttori e consumatori.
Principali caratteristiche di InfoBus
La struttura
di un'applicazione InfoBus comporta di due principali caratteristiche:
Passo 1. Partecipazione (Membership) — Stabilire la partecipazione ad un InfoBus dei componenti
Ogni componente
Java può collegarsi ad un InfoBus. Questo è realizzato
implementando l'interfaccia InfoBusMember, ottenendo un'istanza
InfoBus,
e unendosi (join) a tale istanza.
Passo 2. Ascoltare gli eventi riguardanti l'InfoBus
Una volta che l'oggetto è un elemento di un Infobus, esso riceverà notifiche del bus
Due interfacce ascoltatrici di eventi sono state create per supportare le tue tipologie di applicazioni InfoBus. Il consumatore riceve la notifica della disponibilità dei dati, mentre il produttore riceve la richiesta del dato.
Queste interfacce sono:
InfoBusDataConsumer
e
InfoBusDataProducer
Entrambe estensioni dell'interfaccia InfoBusEventListener
Inoltre esiste anche DataItemChangeListener che gestisce il cambiamento del dato, e visto che InfoBus è vista globalmente come una proprietà bound e soggetta a condizioni, vengono usate anche le liste ascoltatrici:
PropertyChangeListener
e
VetoableChangeListener
Passo 3. Appuntamento per il dato da trasmettere
Nel modello InfoBus, il produttore annuncia la disponibilità del nuovo dato appena il dato è pronto (per esempio dopo il completamento della lettura di un URL, il completamento di un calcolo ecc.)
I consumatori richiedono i dati al produttore al verificarsi di particolare condizioni (inizializzazione dell'applet, evento collegato al bottone, ecc.). L'appuntamento è stabilito tramite il nome del dato.
Quindi, tutti i produttori e consumatori devono fornire dei meccanismi nell'applicazione che possano specificare i nomi dei dati per l'appuntamento. Per esempio in un componente consistente in un foglio elettronico, l'utente può "dare un nome" agli intervalli nel foglio.
Questo nome è
un naturale meccanismo per il riconoscimento dei dati che possono essere
esportati dal foglio che assume il ruolo di produttore.
Passo 4. Navigazione dei dati strutturati
Differenti produttori spesso usano rappresentazioni dei dati che sono solo superficialmente simili.
Per esempio un foglio elettronico e un database ambedue lavorano con le tabelle, ma spesso memorizzano i dati in modo abbastanza differente.
In un foglio elettronico, la tabella dei dati potrebbe essere rappresentata come un output di un calcolo (come una matrice invertita), o come una matrice di formule, laddove in un database la stessa informazione potrebbe essere rappresentata come per esempio il risultato di una query join.
Un consumatore non ha bisogno di capire dettagliatamente la rappresentazione interna del dato preparato dal produttore.
Un componente che gestisce dati dovrebbe poter disegnare un grafico a partire da una tabella indipendentemente se questa è il risultato di un foglio elettronico o di un database.
In pratica questa condivisione di informazioni tra produttori e consumatori richiedono una comune codifica dei dati.
Sono quindi state disegnate una serie di interfacce per vari protocolli standard che sono usati per creare elementi di dati con comuni accessi.
In questo articolo
non viene analizzato questo passo.
Passo 5. Ricerca della codifica per il valore del dato
Un elemento di dato può essere restituito come una String o come un oggetto Java.
Gli oggetti Java sono tipicamente classi che corrispondono ai vari tipi primitivi (per esempio Double che corrisponde al tipo primitivo double), oppure sono istanze di classi viste come collezioni di dati (array). L'obiettivo è la richiesta di specializzati e comprensibili formati dei dati da parte del consumatore.
Per questo vengono
utilizzate l'interfaccia DataItem che forniscono informazioni sul
dato e sui suoi DataFlavor, e l'interfaccia ImmediateAccess che
fornisce informazione dirette su dati semplici trattando solo il formato
String e Object.
Passo 6. Opzionale: La modifica del valore del dato
Un consumatore può cercare di cambiare il valore del dato.
Il produttore impone una politica su chi voglia cambiare il dato. Con il JDK 1.2, esso può anche gestire permessi per i vari consumatori.
La casse che gestisce tale operazione è DefaultPolicy che implementa l'interfaccia InfoBusPolicyHelper.
In questo articolo
non viene analizzato questo passo.
Descrizione package javax.infobus
Interfacce
ArrayAccess DataItem | Questa
interfaccia fornisce informazioni di identificazione e di descrizione del
dato da trasmettere.
I produttori implementano sempre questa interfaccia. |
|
DataItemChangeListener | Gestori
di dati possono implementare opzionalmente DataItemChangeListener
in modo da potersi registrare alla lista tramite i metodi contenuti nell'interfaccia
DateItemChangeManager.
Una classe che implementa DataItemChangeManager spedisce le notifiche del cambiamento del valore attraverso l'evento DataItemChangeEvents.
|
|
DataItemChangeManager | Questa interfaccia permette di registrarsi e di rimuovere la registrazione dalla lista ascoltatrice DataItemChangeListener | |
DbAccess | Gestisce i dati provenienti da un data base | |
ImmediateAccess | Restituisce il dato semplice, non tratta quindi le collezioni di dati, e offre dei metodi per estrarre e per impostare il dato in formato String o Object. | |
InfoBusDataConsumer | Viene implementata dai consumatori, ed è la lista ascoltatrice che gestisce gli eventi di disponibilità e di revoca della disponibilità del dato, notificati dal produttore. | |
InfoBusDataController | Personalizzazioni
alle implementazioni di InfoBusDataController possono essere aggiunte
per ottimizzare la distribuzione dell'evento InfoBusEvent gestito da InfoBusDataProducer
e
InfoBusDataConsumer. Viene utilizzata dalle classi che fungono da mediatori tra produttori e consumatori. |
|
InfoBusDataProducer | Viene implementata dai produttori, ed è la lista ascoltatrice che gestisce l'evento di richiesta del dato, notificato dal consumatore. | |
InfoBusEventListener | E' l'interfaccia comune di InfoBusDataConsumer e InfoBusDataProducer | |
InfoBusMember | E' necessario implementare questa interfaccia per poter gestire una proprietà con condizioni chiamata infoBus . Intorno a questa proprietà gira tutto il presente package. | |
InfoBusPolicyHelper | Interfaccia che pone un insieme di regole da adottare in caso di cambiamento del dato da parte dei consumatori. | |
InfoBusPropertyMap | Interfaccia
temporanea adottata per fornire un meccanismo per l'uso dei componenti
InfoBus 1.1 che desiderino fornire proprietà nell'evento DataItemChangeEvents.
Fornisce il solo metodo get(Object key) che ritorna l'oggetto corrispondente allo specifico nome della proprietà key. |
|
RowsetAccess | Metodi di supporto per la gestione delle righe di tabelle DB | |
ScrollableRowsetAccess | Metodi di supporto per la gestione delle righe di tabelle DB |
Classi
DataItemAddedEvent | Evento che si verifica quando un elemento viene aggiunto ad una collezione di dati. |
DataItemChangeEvent | Evento che si verifica quando avviene un cambiamento del dato. |
DataItemChangeSupport | Classe di supporto per la gestione degli eventi che gestiscono il cambiamento dei dati. |
DataItemDeletedEvent | Evento che si verifica quando un elemento viene cancellato da una collezione di dati. |
DataItemRevokedEvent | Evento che viene notificato dal produttore quando il dato non è più disponibile. |
DataItemValueChangedEvent | Evento che si verifica quando avviene un cambiamento nel valore del dato. E' una sottoclasse di DataItemChangeEvent |
DefaultPolicy | Classe
per la gestione della sicurezza in caso di cambiamento del dato da parte
dei consumatori.
Questa classe implementa l'interfaccia InfoBusPolicyHelper |
InfoBus | Un oggetto InfoBus detiene una lista di InfoBusMember e abilita la comunicazione tra le classi che implementano gli InfoBusMember. |
InfoBusEvent | Evento
di base per la gestione di una comunicazione InfoBus. Viene gestito
dalla lista ascoltatrice InfoBusEventListener
In genere questo evento non viene utilizzato, perché vengono utilizzate le sue sottoclassi specializzate (InfoBusItemAvailableEvent, InfoBusItemRevokedEvent, InfoBusItemRequestedEvent) |
InfoBusItemAvailableEvent | Evento che notifica la disponibilità di un dato. Spedito dal produttore ad uso dei consumatori registrati alla lista ascoltatrice InfoBusEventListener. |
InfoBusItemRequestedEvent | Evento che notifica la richiesta di un dato. Spedito dal consumatore ad uso dei produttori registrati alla lista ascoltatrice InfoBusEventListener. |
InfoBusItemRevokedEvent | Evento che notifica la revoca della disponibilità di un dato. Spedito dal produttore ad uso dei consumatori registrati alla lista ascoltatrice InfoBusEventListener. |
InfoBusMemberSupport | Questa classe implementa l'interfaccia InfoBusMember e serve a gestire le funzionalità del protocollo InfoBus protocol. Incapsula luna proprietà InfoBus e gli oggetti PropertyChangeSupport e VetoableChangeSupport dato che tale proprietà è bound e soggetta a condizione. |
RowsetCursorMovedEvent | Descrive un cambiamento di valore in un elemento che può anche essere una collezione. |
Eccezioni
ColumnNotFoundException | Usata per la gestione dei DB, viene sollevata nel caso in cui la colonna non viene trovata. |
DuplicateColumnException | Usata per la gestione dei DB, viene sollevata nel caso in cui si tenta di inserire una colonna già presente. |
InfoBusMembershipException | Viene sollevata quando si tenta di fare una join su di un InfoBus ormai molto vecchio oppure quando non si ha il permesso di fare la join. |
InvalidDataException | Viene sollevata da un DataItem quando si cerca di cambiarne il valore con un formato illegale. |
RowsetValidationException | Usata per la gestione dei DB, viene sollevata nel caso in cui la modifica di un valore fallisce per qualche ragione. |
UnsupportedOperationException | Può essere sollevata se il partecipante all'Infobus non supporta il metodo chiamato |
Descrizione esempio TimeSource-Clock
Analizziamo ora un esempio presente nel pacchetto scaricabile da Internet: la collaborazione tra un'applet che deposita il tempo in un Infobus ogni secondo, e un'altra applet che lo prende e lo utilizza.
In questo esempio si dimostra quindi la collaborazione tra un produttore, TimeSource.class che scrive un elemento rappresentante l'ora corrente (CurrentTime) in un InfoBus ogni secondo che passa. Il consumatore è l'applet Clock.class, che cerca l'ora corrente nell'InfoBus, si registra ad una lista che gestisce i cambiamenti del dato, e visualizza l'ora esatta, ogni volta che gli viene notificato il cambiamento di questa.
Ripeteremo i
passi di cui abbiamo parlato sopra, mostrando e commentando il codice scritto.
Passo 1. Partecipazione— Stabilire la partecipazione ad un InfoBus dei componenti
Per stabilire
la partecipazione ad un Infobus è necessario prima di tutto implementare
l'interfaccia InfoBusMember,
Produttore
TimeSource
public class
TimeSource extends Applet implements InfoBusMember
Consumatore
Clock
public class Clock extends Applet implements InfoBusMember
Dopo di che bisogna di far partecipare la classe TimeSource e Clock all'Infobus.
A tale scopo viene utilizzata la classe InfoBusMemberSupport che viene associata all'applet TimeSource o Clock e viene utilizzato il metodo joinInfoBus() dell'interfaccia InfoBusMemberSupport per unire l'interfaccia all'Infobus di default. Questa operazione che avviene nel metodo init() viene poi ritentata nel metodo start() se per caso precedentemente non c'era ancora un Infobus disponibile:
// definizione della classe InfoBusMemberSupportPasso 2. Ascoltare gli eventi riguardanti l'InfoBusprivate InfoBusMemberSupport m_InfoBusHolder = null;
// Associazione dell'applet TimeSource o Clock alla classe InfoBusMemberSupport
m_InfoBusHolder = new InfoBusMemberSupport( this );
// Operazione di join dell'Applet all'InfoBus di default
try
{
m_InfoBusHolder.joinInfoBus( this );
}
catch ( InfoBusMembershipException e )
{
System.out.println("Clock.init warning: "+
"joinInfoBus failed because InfoBus already set: " + e.toString());
}
catch ( PropertyVetoException pve )
{
System.out.println("Clock.init warning: "+
"joinInfoBus failed because voter vetoed setInfoBus: " + pve.toString());
}
Le liste ascoltatrici
supportate sono:
A tale scopo viene utilizzata la classe InfoBusMemberSupport
// Registrazione dell'applet TimeSource o Clock alla lista che intercetta i cambiamenti
// della proprietà dell'InfoBus.
// Istruzione definita nel metodo init()
m_InfoBusHolder.addInfoBusPropertyListener(
this );
Per registrare
e cancellare la registrazione di altre classi affinché vengano notificate
dei cambiamenti delle proprietà, sono stati aggiunti i metodi:
public void addInfoBusPropertyListener(PropertyChangeListener l)
{
m_InfoBusHolder.addInfoBusPropertyListener ( l );
}
public void removeInfoBusPropertyListener(PropertyChangeListener l)
{
m_InfoBusHolder.removeInfoBusPropertyListener ( l );
}
Dopo di che è stato ridefinito il metodo propertyChange():
public void propertyChange ( PropertyChangeEvent pce )
{
String s = pce.getPropertyName();
if ( ! s.equals("InfoBus") )
{
return;
}
Object oldVal = pce.getOldValue();
Object newVal = pce.getNewValue();
if ( oldVal == newVal )
{
return;
}
if ( oldVal != null && oldVal instanceof InfoBus )
{
((InfoBus) oldVal).removeDataProducer ( this );
}
if ( newVal != null && newVal instanceof InfoBus )
{
try
{
// queste 2 istruzioni sono alternative a secondo se si parla di produttore
// (TimeSource) o consumatore (Clock)
((InfoBus) newVal).addDataProducer ( this );
((InfoBus) newVal).addDataConsumer ( this );
}
catch ( InfoBusMembershipException e )
{
}
}
}
Per registrare e cancellare la registrazione di altre classi affinchè vengano notificate dei cambiamenti delle proprietà con restrizioni, sono stati aggiunti i metodi:
public void addInfoBusVetoableListener(VetoableChangeListener l)
{
m_InfoBusHolder.addInfoBusVetoableListener ( l );
}
public void removeInfoBusVetoableListener(VetoableChangeListener l)
{
m_InfoBusHolder.removeInfoBusVetoableListener ( l );
}
Dopo aver ottenuto l'istanza di Infobus in questo modo:
InfoBus ib = m_InfoBusHolder.getInfoBus();
dove m_InfoBusHolder
ricordiamo che è l'istanza della classe InfoBusMemberSupport,
il programma nel metodo start() effettua la registrazione della
classe e nel metodo stop() la sua rimozione:
// registrazione produttore
ib.addDataProducer(this);
// rimozione produttore
getInfoBus().removeDataProducer(this);
// registrazione consumatore
ib.addDataConsumer(this);
// rimozione consumatore
getInfoBus().removeDataConsumer(this);
Entrando in
merito ai metodi contenuti nelle interfacce, possiamo dire che mentre l'interfaccia
InfoBusDataProducer
contiene il metodo per intercettare l'evento in cui viene richiesto
il dato (dataItemRequested()), l'interfaccia
InfoBusDataConsumer
contiene il metodo per gestire l'evento che notifica la disponibilità
del dato (dataItemAvailable()), e quello per gestire l'evento che
notifica che il dato non è più disponibile(dataItemRevoked()).
Il codice relativo per le due applet è quello che segue.
Produttore TimeSource
Se qualche classe richiede il dato CurrentTime viene richiamato il metodo dataItemRequested dell'interfaccia InfoBusDataProducer, ridefinito così:
public void dataItemRequested (InfoBusItemRequestedEvent e)
{
// Vengono prese in considerazione solo richieste per elementi chiamati "CurrentTime"
if (!e.getDataItemName().equals("CurrentTime") )
return;
e.setDataItem(this);
}
Consumatore Clock
Non appena il produttore rende accessibile il dato questo scatena l'evento InfoBusItemAvailableEvent, intercettato dal metodo dataItemAvailable():
public void dataItemAvailable (InfoBusItemAvailableEvent e)
{
// Vengono prese in considerazione solo dati accessibili chiamati "CurrentTime"
if (!e.getDataItemName().equals("CurrentTime") )
return;
// rimuove questa classe dalla lista ascoltatrice se aveva un vecchio CurrentTime
if ( ( m_DataItem != null ) && ( m_DataItem instanceof DataItemChangeManager ) )
{
((DataItemChangeManager) m_DataItem).removeDataItemChangeListener( this );
}
// ottiene il corrente CurrentTime passato dall'evento e si registra di nuovo
// alla lista ascolatrice.
m_DataItem = e.requestDataItem ( this, null );
if (( m_DataItem != null ) && ( m_DataItem instanceof DataItemChangeManager ) )
{
((DataItemChangeManager) m_DataItem).addDataItemChangeListener( this );
}
}
Non appena il
produttore revoca la disponibilità del dato questo scatena l'evento
InfoBusItemRevokedEvent,
intercettato dal metodo dataItemRevoked ():
public void dataItemRevoked ( InfoBusItemRevokedEvent e )
{
// Vengono prese in considerazione solo dati accessibili chiamati "CurrentTime"
if (!e.getDataItemName().equals("CurrentTime") )
return;
revokeCurrentTime();
}
void revokeCurrentTime()
{
if (null != m_DataItem)
{
// "CurrentTime" è stato revocato; rimuove la classe dalla lista ascoltatrice
// e rilascia il puntarore al dato
if ( m_DataItem instanceof DataItemChangeManager )
{
((DataItemChangeManager) m_DataItem).removeDataItemChangeListener(this);
}
m_DataItem = null;
repaint(); // repaint che indica che nessuna ora è accessibile.
}
}
La sola applet TimeSource produttore implementa l'interfaccia DataItemChangeManager che contiene i metodi per registrarsi e cancellare la registrazione dalla lista ascoltatrice DataItemChangeListener, in quanto è TimeSource la classe detentrice della lista.
I metodi ridefiniti
nel programma sono:
Produttore
TimeSource
public void addDataItemChangeListener(DataItemChangeListener l)
{
// m_changedListeners è un vettore che gestisce gli ascoltatori
if (null==m_changedListeners)
m_changedListeners = new Vector( 5,5 );
m_changedListeners.addElement(l);
}
e
public void removeDataItemChangeListener(DataItemChangeListener l)
{
if (null!=m_changedListeners)
{
// rimuove la lista
m_changedListeners.removeElement(l);
// se rimosso l'ultimo elemento, cancella il vettore
if (m_changedListeners.isEmpty())
m_changedListeners = null;
}
}
Dopo di che appena si desidera notificare il valore del tempo cambiato viene richiamato il metodo setTimeValue() e qui vengono avvisati tutti i consumatori del dato:
if (null!=m_changedListeners)
{
// Crea il change event
DataItemValueChangedEvent dice =
new DataItemValueChangedEvent(this, this, null);
// Enumera gli elementi.
Enumeration enum = m_changedListeners.elements();
// Per ciascun ascoltatore registrato, spedisce il change event.
while (enum.hasMoreElements())
{
DataItemChangeListener listener = (DataItemChangeListener)enum.nextElement();
listener.dataItemValueChanged(dice);
}
}
Consumatore
Clock
Il consumatore è interessato a essere registrato alla lista ascoltatrice DataItemChangeListener perché in questo modo viene a conoscenza del fatto che il dato è cambiato. In tal caso viene richiamato il metodo dataItemValueChanged() ridefinito in questo caso come segue:
public void dataItemValueChanged(DataItemValueChangedEvent event)
{
// Il metodo paintme ottiene il valore di CurrentTime
// e visualizza la nuova ora nell'orologio.
repaint();
}
Nel metodo paint() (chiamato in seguito a repaint()) viene richiamato il metodo paintme() dove viene recuperato il valore del dato CurrentTime. Più avanti spiegheremo più dettagliatamente la modalità di come avviene.
Abbiamo detto che l'appuntamento è stabilito tramite il nome del dato.
In questo programma
il nome del dato è CurrentTime e consiste in una variabile
Long che trasmette il tempo in millisecondi. Il valore di questa
variabile viene trasmesso ogni secondo.
Produttore TimeSource
Per annunciare la disponibilità del dato viene utilizzato il metodo fireItemAvailable() appartenente all'interfaccia Infobus e utilizzato nel metodo run():
getInfoBus().fireItemAvailable("CurrentTime",null, this);Una volta che il dato viene reso disponibile, vengono avvisati tutti i consumatori registrati alla lista ascoltatrice DataItemChangeListener. Nel passo 2. viene mostrato il codice.
Consumatore Clock
Nel metodo start() viene richiamato il metodo dove findCurrentTime() il consumatore prova subito a vedere se il dato è disponibile chiamando il metodo findDataItem() metodo dell'Infobus:
m_DataItem = getInfoBus().findDataItem("CurrentTime", null, this);La richiesta di questa informazione provoca un evento DATA_REQUEST, in seguito al quale viene chiamato il metodo dataItemRequested() definito nel produttore.
Come detto prima
quando il dato cambia valore il consumatore viene avvisato del cambiamento
perché viene richiamato il suo metodo dataItemValueChanged()
Passo 5. Ricerca
della codifica per il valore del dato
Produttore TimeSource
Questa classe
implementa DataItem che fornisce informazioni sui dati da scambiare.
In questo programma però si è deciso di non utilizzare i
metodi di questa interfaccia che vengono ridefiniti e restituiscono null
(tranne
il metodo getSource() che restituisce this come
InfoBusEventListener,
cosa che indica TimeSource come la classe sorgente dell'evento InfoBusEvent)
MokaByte Web 1998 - www.mokabyte.it MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it |