Prerequisiti

Per poter colloquiare con AS/400 da una piattaforma pc è necessario che sulla stessa sia installato il driver ODBC del Client Access.
Il Client Access è un prodotto IBM che viene fornito gratuitamente assieme all’AS/400.
Per mia esperienza personale (lavoro su AS/400 e su pc da 9 anni) possono considerarsi accettabilmente stabili le versioni di Client Access dalla V4R5 in poi (non che le versioni precedenti non lo siano, ma queste ultime di problemi ne danno veramente quasi nessuno) con particolare riguardo, anche se con qualche piccolo accorgimento in casi specifici, alla versione V5R1.
Nell’installazione tipica del Client Access vengono installati una serie di prodotti tra cui appunto il driver ODBC ma nessuno vieta – in caso non si vogliano mettere sul pc a disposizione dell’utente particolari funzioni, o si intenda in certi casi limitarne le possibilità – di poter installare unicamente il driver ODBC.
Mi spiego meglio: nell’installazione tipica viene sempre installata di default anche l’emulazione terminale 5250, che consente all’utente di collegarsi tramite sessione video e lavorare interattivamente sull’AS/400; considerando che vi state interfacciando ad una piattaforma per antonomasia “sicura”, questa installazione potrebbe costituire una cosa poco gradita per l’azienda per la quale state sviluppando; quindi, se non diversamente specificato, limitatevi ad installare unicamente il driver ODBC del Client Access (potrebbe sembrare superfluo o quasi fuori tema, ma segnalo questo particolare perché più di una volta a persone che conosco è capitato di essere contestati dall’azienda per aver messo a disposizione dell’utente strumenti che invece non era opportuno dargli).
Nel seguito, a questo proposito, troverete un ulteriore suggerimento per evitare di rendere poco sicura una piattaforma di cui la sicurezza è una caratteristica preminente.
<TITLE1>Un po’ di fondamentali…
Per quanto possa sembrare ovvio che, prima di poter pensare di fare sviluppo interfacciandosi con una piattaforma AS/400, occorra conoscere quest’ultima almeno sommariamente, forse non è inopportuno richiamare alcune nozioni basilari, prima di addentrarsi sui dettagli meramente tecnici della questione che si sta trattando.
In particolare, cercherò di spiegare in modo semplice cosa si intende per file:
<UL>
<LI>forse non tutti coloro che lavorano unicamente su piattaforme pc sanno (e può risultare quasi “ostico”) che quelli che in ambiente pc vengono comunemente chiamati file (file di testo, file eseguibili, etc…) su AS/400 vengono chiamati oggetti. Ci sono vari tipi di oggetti ma gli unici che vengono chiamati file sono quelle strutture atte a contenere dati, ergo DB.
<LI>da un punto di vista gestionale e non sistemistico su AS/400 la struttura gerarchica è articolata solo su 2 livelli.
1.    Il primo livello è qualcosa di simile a quelle che su pc tutti quanti chiamiamo “cartelle” o “directory”, che su AS/400 si chiamano invece “librerie” e, a differenza delle piattaforme pc (io naturalmente parlo di ambienti basati su sistemi operativi Microsoft in quanto sono gli unici che conosco), in cui “cartelle” o qualsivoglia “directory” possono contenere a loro volta altre “cartelle”, su AS/400 no!!! <br>2.    Il secondo livello è rappresentato quindi dagli oggetti contenuti nelle librerie (in realtà anche le librerie sono oggetti, ma allora diremo più esattamente che gli oggetti di tipo libreria possono contenere qualsiasi tipo di oggetto tranne altri oggetti di tipo libreria, appunto).
<LI>infine, ma solo per quanto riguarda gli oggetti di tipo file, esiste un ulteriore livello al quale troviamo quelli che sono chiamati “membri”; ogni file può contenere da 0 a 32767 membri (ognuno naturalmente avente diverso nome ma altrettanto naturalmente medesima struttura del file in cui sono diciamo “contenuti”); se il file contiene 0 membri, significa in sostanza che il file è solo struttura (tracciato record), non contiene dati e non è nemmeno propenso a contenerne fino a quando non verrà aggiunto al file almeno 1 membro. Ogni membro, infine – per fare un paragone più vicino al mondo dei pc – è come se fosse un file a sé stante, ovvero deve essere esplicitamente “puntato” tramite il comando di sistema (OS/400) OVRDBF. Questo comando, come indicato dal nome stesso, è un comando di sovrapposizione, ovvero imposta un <override data-base file> con il quale si specifica a quale membro del file “puntare” quando si fa riferimento semplicemente al nome file; semplificando il concetto sovrappone il binomio “nome file/membro” al file x cui, fino alla revoca della sovrapposizione (tramite il comando di sistema DLTOVR), si avrà che “nome file” = “nome file/membro”. Qualora infatti riferendosi a un file si tralasciasse di “puntare” esplicitamente il membro, verrebbe assunto di default il membro avente la caratteristica di “*first” ovvero il primo (attenzione, però: in ordine temporale di creazione, non in ordine alfabetico o altro). Sicuramente si potrebbe obiettare (ed a ragione!) che questi comandi di sovrapposizione possono essere piuttosto “pericolosi” (infatti rendono opportuni alcuni accorgimenti che non sto qui a illustrare perché esulano troppo dall’argomento dell’articolo).
</UL>
Un limite fortemente sentito da quanti come me lavorano su AS/400 di questa struttura a ‘membri’ o, a seconda dei punti di vista, del prodotto SQL/400 messo a disposizione, è il fatto di non poter direttamente riferirsi ad un determinato membro di un file appunto multi-membro (è verò però che l’uso di file multi-membro NON è poi così diffuso… probabilmente anche per questo motivo).
<TITLE1>La connessione con AS/400
Questa è sicuramente la fase principale; voi direte: “per forza: senza prima una connessione, cosa si pretende di poter fare?!?”; avete ragione, ma, a parte questa ovvietà, io intendo far notare che la maggior parte dei problemi in progetti VB che colloquiano con AS/400 sono causati dalla connessione, o meglio dalla gestione delle connessioni.
La connessione costituisce infatti, oltre che la base fondamentale di qualsiasi operazione con AS/400, anche il momento più critico sia in termini di tempo che di risorse; tra le altre cose noterete che l’apertura della connessione con AS/400 è la fase in assoluto più lenta (questo perché su AS/400 ciò implica tutta una serie di cose di cui vi risparmio volentieri l’elenco).
E’ per questo che, sempre per esperienza, consiglio di lesinare con le connessioni: è in assoluto preferibile aprire e mantenere aperta una connessione tra il pc e AS/400 anche per lungo tempo, piuttosto che aprire e chiudere continuamente nuove connessioni.
Nell’impostazione della stringa di connessione un’attenzione particolare la dedicherei alle informazioni per effettuare il login.
Naturalmente si potrà scegliere tra l’impostazione di user e password in chiaro

<boxTT>
sConnectionString = “Driver={Client Access ODBC Driver (32-bit)}; System=AS001;User ID=PIPPO;Password=PLUTO”
<endbox>
oppure (mi sembra un po’ brutale) attraverso l’uso di parametri
<boxTT>
sConnectionString = “Driver={Client Access ODBC Driver (32-bit)};System=” & INI_strSystemAS & “;User ID=” & INI_strUserAS & “;Password=” & INI_strPasswordAS
<endbox>

dove le variabili INI_xxx sono state valorizzate attraverso la lettura di un banale file INI appositamente creato per la nostra applicazione; oppure ancora si potrà scegliere di non impostare affatto i datidi login; in questo caso sarà lo stesso Client Access che all’atto della richiesta di apertura della connessione richiederà a video utente e password (o talvolta, dipende dalle impostazioni e dalla versione di Client Access, assumerà automaticamente le informazioni di login di una eventuale connessione verso AS/400 già attiva).
Far inserire all’utente utente e password presenta vantaggi e svantaggi; il vantaggio è costituito dalla tracciabilità, nel senso che tutto ciò che viene fatto su AS/400 durante questa connessione verrà attribuito all’utente della connessione stessa, cioè all’utente che di volta in volta utilizza l’applicazione VB; lo svantaggio è quello di non poter, per lo stesso identico motivo, “pilotare” (decidere) consapevolmente l’utente al quale poi potranno essere attribuite tutte le operazioni svolte durante la transazione.
Solitamente, quando una procedura VB deve svolgere operazioni che prescindono dall’utente, mi permetto di consigliare (come avevo accennato nel paragrafo Prerequisiti) di far “girare” la procedura VB tramite una connessione AS/400 aperta con un utente “sicuro”, cioè, questioni di autorizzazioni speciali sugli oggetti a parte, un utente per il quale non sia previsto su AS/400 il SIGNON.
Mi spiego meglio con un esempio: io solitamente inserisco nome del sistema AS/400, utente e password da utilizzare nel file INI di procedura.
Ipotizziamo che l’utente per qualche suo recondito motivo vada a sbirciare nel file INI e che riesca a reperire tali informazioni e che, ancora, tenti di effettuare una sessione video con l’AS/400 (anche se non tramite l’emulazione del 5250 del Client Access, magari via TELNET).
In questo caso, il fatto che per l’utente da me appositamente scelto ed indicato nel file INI sia impostato su AS/400 con attributo SIGNON  = *OFF non darà all’utente la possibilità di collegarsi interattivamente in alcun modo.
Potrebbe sembrare una cosa di poco conto, ma prestate attenzione al fatto che, potenzialmente, un’azienda potrebbe voler dare un proprio pc con installata una sua particolare procedura direttamente ad un suo cliente e che questo pc possa venire utilizzato da diverse persone… beh, capite bene che per aziende che hanno i loro sistemi informativi “core” su AS/400 la sicurezza non è mai troppa. Questo accorgimento è un modo per garantirla.
Facendola breve la filosofia è: perché avere una piattaforma sicura quando fornendo utente e password inadeguati all’uso la si rende potenzialmente insicura? Vediamo di evitarlo.
Se a questo punto qualcuno di voi si stesse chiedendo “ma di codice VB quando si parla???”, avrebbe assolutamente ragione. Ecco qui un esempio di codice per l’impostazione e la creazione della connessione verso un sistema AS/400: Nelle dichiarazioni generali:

<boxTT>
‘ Variabili relative alle connessioni ed ai recordset su AS/400
Private mcnDbAS As ADODB.Connection
Private mrsTbAS As ADODB.Recordset
Private mbConnessoAS As Boolean
Private mbConnessoRs  As Boolean
‘In una apposita funzione:
Public Function CollegaAS() As Boolean
On Error GoTo CollegaAS_Err
CollegaAS = False
mbConnessoAS = False
‘ In fase di creazione della connessione testo sempre se esiste già una
‘ connessione aperta, se esiste la chiudo e la resetto
If Not mcnDbAS Is Nothing Then
If mcnDbAS.State = adStateOpen Then
mcnDbAS.Close
End If
Set mcnDbAS = Nothing
End If

‘ Creo una nuova connessione
Set mcnDbAS = CreateObject(“ADODB.Connection”)
‘ Quindi procedo con l’impostazione della stringa di connessione parametrizzata
‘ (creo un DSN di sistema temporaneo per la connessione con AS/400)
‘ dove:
‘      INI_strSystemAS   è il parametro dell’INI indicante il nome dell’AS/400
‘                        sulla rete
‘      INI_strUserAS     è il parametro dell’INI indicante l’utente con cui
‘                        effettuare la connessione
‘      INI_strPasswordAS è il parametro dell’INI indicante la password
‘                        dell’utente di cui sopra
mcnDbAS.ConnectionString = “Driver={Client Access ODBC Driver (32-bit)};” & _
“System=” & INI_strSystemAS & “;User ID=” & _
INI_strUserAS & “;Password=” & INI_strPasswordAS

‘ Ed infine apro il canale SQL con AS400
mcnDbAS.Open
DoEvents
CollegaAS = True
mbConnessoAS = True
Exit Function

CollegaAS_Err:
‘ Registro nel file di log della procedura l’errore occorso
Call ScriviLog(Format$(Now, “yyyy/mm/dd hh:mm:ss”) & ” – Procedura: ” & _
App.EXEName & ” – Routine: CollegaAS – Errore: ” & Err.Description)
‘ Mando a video l’errore
MsgBox Err.Number & ” – ” Err.Description
‘ Se ho un errore o sulla connessione AS/400 o sul Recordset
‘ chiudo comunque e sempre entrambi
If Not mrsTbAS Is Nothing Then
If mrsTbAS.State = adStateOpen Then
mrsTbAS.Close
End If
Set mrsTbAS = Nothing
mbConnessoRs  = False
End If
If Not mcnDbAS Is Nothing Then
If mcnDbAS.State = adStateOpen Then
mcnDbAS.Close
End If
Set mcnDbAS = Nothing
mbConnessoAS = False
End If
End Function
<endbox>

Ho preferito gestire la connessione in una Function in quanto, come dicevo prima, la mia tendenza è di creare una connessione e di mantenerla aperta e di usarla per tutto il tempo necessario o fino ad un eventuale errore che comporti il tentativo di apertura di una nuova connessione.
Infatti io di solito implemento la creazione della connessione subito nella Sub Main. Nell’esempio che segue, implemento anche quella del recordset poiché la procedura in questo caso deve anche, ogni tot secondi, ciclare e accodare dei record sempre sullo stesso file su AS/400:
<boxTT>Sub Main()
‘ Apro subito la connessione con AS/400 e se riesce
‘ apro subito anche il recordset remoto
If CollegaAS Then
Call CollegaRs
End If
‘ …etc…
<endbox>

<TITLE1>I recordset
Innanzitutto occorre fare una importante precisazione in relazione a quanto precedentemente esposto nel paragrafo “Un po’ di fondamentali…” relativamente ai membri di un file: essendo l’ODBC per sua natura basato diciamo grossolanamente su SQL avremo anche qui le stesse limitazioni. Una volta impostata la connessione, occorre passare all’impostazione dei recordset in modo ottimale. Quest’ultimo termine potrebbe sembrare banale o superfluo, ma è opportuno tener conto che le situazioni si complicano già quando si tratta di dover gestire transazioni cosiddette remote; se poi ci si aggiunge il fatto di farlo tra piattaforme diverse… beh, i livelli di astrazione che vengono interessati si moltiplicano, ragion per cui l’ottimizzazione assume ancora maggior rilievo.
E’ quindi importante distinguere l’uso che faremo dei vari recordset che ci occorrerà definire; supponiamo che l’elaborazione “core” stia dalla parte cliente (assumiamo per convenzione che il lato client sia la piattaforma pc e che il lato server sia l’AS/400), che i dati oggetto dell’elaborazione stiano su AS/400 e che ci interessi unicamente leggere, tali dati.
In questo caso la soluzione più indicata (in quanto più efficiente) è la seguente:

<boxTT>
‘ Il parametro bReadOnly è subordinato all’uso a cui il record è destinato
Public Function CollegaRs(bReadOnly As Boolean) As Boolean
On Error GoTo CollegaRs_Err
CollegaRs = False
mbConnessoRs  = False

‘ In fase di definizione del recordset testo sempre
‘ se esiste già un recordset aperto,
‘ se esiste lo chiudo e lo resetto
If Not mrsTbAS Is Nothing Then
If mrsTbAS.State = adStateOpen Then
mrsTbAS.Close
End If
Set mrsTbAS = Nothing
End If

‘ Creo un nuovo recordset
Set mrsTbAS = CreateObject(“ADODB.Recordset”)
‘ Imposto gli attributi per il recordset remoto su AS/400
‘ Se richiesto record per operazioni di sola lettura
If bReadOnly Then
‘Non effettua allocazioni sui record dalla parte AS/400
mrsTbAS.LockType = adLockReadOnly
mrsTbAS.CursorLocation = adUseClient
mrsTbAS.CursorType = adOpenStatic
Else
mrsTbAS.LockType = adLockOptimistic
‘ E’ l’UNICA modalità da usare in caso si voglia scrivere o aggiornare
‘ (MAI usare asLockBatchOptimistic)
mrsTbAS.CursorLocation = adUseClient
mrsTbAS.CursorType = adOpenKeyset
End If

‘ Imposto la connessione su cui il recordset si baserà
Set mrsTbAS.ActiveConnection = mcnDbAS

‘ Apro il file remoto su AS400 in base alla stringa parametrizzata
‘ dove:
‘ INI_strLibraryAS è il parametro dell’INI indicante la libreria in cui
‘ è contenuto il file
‘ INI_strFileAS è il parametro dell’INI indicante il file
mrsTbAS.Open “SELECT * FROM ” & INI_strLibraryAS & “.” & _
INI_strFileAS & “. WHERE & etc…”
DoEvents
CollegaRs = True
mbConnessoRs  = True
Exit Function

CollegaRs_Err:
‘ Registro nel file di log della procedura l’errore occorso
Call ScriviLog(Format$(Now, “yyyy/mm/dd hh:mm:ss”) & ” – Procedura: ” &
App.EXEName & ” – Routine: CollegaRs – Errore: ” & Err.Description)
‘ Mando a video l’errore
MsgBox Err.Number & ” – ” Err.Description
‘ Se ho un errore o sulla connessione AS/400 o sul Recordset
‘ chiudo comunque e sempre entrambi
If Not mrsTbAS Is Nothing Then
If mrsTbAS.State = adStateOpen Then
mrsTbAS.Close
End If
Set mrsTbAS = Nothing
mbConnessoRs  = False
End If
If Not mcnDbAS Is Nothing Then
If mcnDbAS.State = adStateOpen Then
mcnDbAS.Close
End If
Set mcnDbAS = Nothing
mbConnessoAS = False
End If
End Function
<endbox>

Nota su adLockBatchOptimistic: vi garantisco che se usate come metodo di allocazione adLockBatchOptimistic il risultato su AS/400 è del tutto imprevedibile, soprattutto se vi è la possibilità che a quel file accedano contestualmente altri programmi (siano essi dal lato pc che dal lato AS/400); effettuando parecchi test in questa modalità, più di una volta e con mio grande stupore ho constatato che i records su AS/400 non venivano nemmeno scritti, senza alcun tipo di errore o riscontro negativo dalla procedura VB o su AS/400.
Invece, adottando il metodo adLockOptimistic, MAI, in NESSUN caso ho avuto alcun tipo di problema o di anomalia, anche in condizione di stress la serializzazione delle operazioni di I/O veniva assolutamente rispettata.

<TITLE1>Le operazioni di I/O
Prima o poi si arriva sempre al dunque: la lettura/scrittura di dati su file AS/400.
In caso di operazioni di aggiornamento/scrittura, io gestisco sempre il Commit della transazione; nell’esempio che segue, si legge un file di testo e si aggiungono record al file AS/400:

<boxTT>Sub Elabora()
Dim btrans As Boolean
Dim buffer As String
On Error GoTo Elabora_Err

‘ Verifico se la connessione con AS/400 ed il recordset sono attivi,
‘ altrimenti cerco d attivarli
If mbConnessoAS = True And mbConnessoRs  = True Then
Else
If CollegaAS Then
Call CollegaRs(false)
End If
End If

‘ Procedo se e solo se sia la connessione con AS che il recordset sono attivi
If mbConnessoAS = False Or mbConnessoRs  = False Then
‘ Lo considero errore
GoTo Elabora_Err
End If

‘ Avvio il gestore commit
mcnDbAS.BeginTrans
btrans = True

‘ Apro in lettura il file di input e aggiungo record al file su AS/400
Open (App.Path & “\” & NomeFileCorrente) For Input As #1
Do While Not EOF(1)
Line Input #1, buffer
If Len(Trim$(buffer)) > 0 Then
mrsTbAS.AddNew
mrsTbAS.Fields(Campo1) = Mid$(buffer, 1, 3)
mrsTbAS.Fields(Campo2) = Mid$(buffer, 4, 3)
mrsTbAS.Fields(Campo3) = Mid$(buffer, 7, 2)
‘ …etc…
End If
Loop
Close #1

‘ Eseguo l’aggiornamento dei record aggiunti
mrsTbAS.Update
‘ Se tutto bene procedo con il commit altrimenti eseguo rollback
mcnDbAS.CommitTrans
btrans = False

Elabora_Err:
‘ Verifico se avviato commit
If btrans = True Then
mcnDbAS.RollbackTrans
End If
‘ Registro nel file di log della procedura l’errore occorso
Call ScriviLog(Format$(Now, “yyyy/mm/dd hh:mm:ss”) & ” – Procedura: ” & _
App.EXEName & ” – Routine: Elabora – Errore: ” & Err.Description)
‘ Imposto a false le variabili che mi indicano lo stato del recordset e della
‘ connessione in modo da forzare il tentativo di creazione nuovi connessione
‘ e recordset su AS/400
mbConnessoAS = False
mbConnessoRs  = False
End Function
<endbox>

<TITLE1>Esecuzione di comandi remoti
Talvolta può risultare necessario eseguire comandi remoti su AS/400 sincroni; l’esempio più comune può essere quello di dover lanciare un programma RPG (l’RPG è il linguaggio nativo di AS/400) per preparare la base di dati in funzione di una certa elaborazione (si tratta quindi di una elaborazione sul lato AS/400), la base dati così aggiornata è poi accessibile da VB; oppure, viceversa, dopo aver scritto dati da pc verso AS/400, dover lanciare una elaborazione dal lato AS/400 di cui la procedura VB non vuole o non deve farsi carico.
In tutti questi casi si può sfruttare la connessione per il lancio di comandi remoti sincroni; facendo riferimento al secondo caso sopra ipotizzato, un comando sincrono potrebbe semplicemente essere la sottomissione in batch dell’elaborazione, per cui l’esito del comando sincrono sarà unicamente l’informazione della corretta o meno sottomissione dell’elaborazione e non il risultato dell’elaborazione stessa.
Vediamo con un semplice codice di esempio di quale sia l’oggetto AS/400 da sfruttare e come mettere in pratica la chiamata da VB:

<boxTT>Function ExeRmtCMD(In_strLIBPGM3)
‘ INI_strLIBPGM3 è un parametro dell’INI di procedura indicante
‘ la stringa di comando in formato AS/400
‘ da lanciare al termine della scrittura dei record
‘ da pc verso AS/400 ad esempio:
‘ CALL LIBRERIA/PROGRAMMA PARM(“PARM1” “PARM2” “PARM3”)

Dim fine As String

‘ Qui determino la lunghezza della stringa di commando in formato AS/400
‘ in quanto l’oggetto QSYS.QCMDEXC,
‘ – ovvero il comando di sistema QCMDEXEC presente nella libreria QSYS
‘   (piccola nota: su AS/400 tutto ciò il cui nome inizia con la lettera
‘   “Q” è un oggetto di sistema) –
‘ prevede 2 parametri:
‘   il primo parametro è appunto la stringa di comando da eseguire
‘           (naturalmente come già detto in sintassi AS/400)
‘   il secondo parametro è la lunghezza effettiva del parametro precedente
‘           in formato numerico 10 interi e 5 decimali
‘           (non ci sono spiegazioni ai 5 decimali!!!)
fine = “‘, ” & Format(Len(In_strLIBPGM3), “0000000000”) & “.00000)”

If In_strLIBPGM3 <> vbNullString And Not mcnDbAS Is Nothing Then
‘ lancio in maniera sincrona il commando remoto
‘ attraverso la connessione corrente
mcnDbAS.Execute “Call QSYS.QCMDEXC(‘” & In_strLIBPGM3 & fine
‘ testo se occorso un errore
If Err > 0 Then
‘ Registro nel file di log della procedura l’errore occorso
Call ScriviLog(Format$(Now, “yyyy/mm/dd hh:mm:ss”) & ” – Procedura: ” & _
App.EXEName & ” – Routine: ExeRmtCMD – Errore: ” & Err.Description)
End If
‘ …etc…
End Function
<endbox>

<TITLE1>Tips & Tricks
Nei vari utilizzi che ho fatto, su diverse procedure VB che ho sviluppato che si interfacciano ad AS/400, adottando strutture e logiche come quelle sopra descritte, non ho mai riscontrato grossi problemi.  L’unica anomalia che di tanto in tanto talvolta capita è quella che il Client Access restituisca l’errore RC= 10055 CWBCO1003 (altra piccola nota: tutto ciò ha il suffisso “cwb” è solitamente riferito al Client Access). Questo errore può saltuariamente capitare in caso ci si trovi su architetture WAN ed il pc in questione non sia esattamente un “mostro” di potenza o, ancora, ci siano delle congestioni di rete; trattasi di un problema di Winsock su cui Client Access si appoggia per le comunicazioni verso AS/400. A tal proposito propongo un articolo dell’IBM che a me è stato molto utile per risolvere l’inconveniente (nell’articolo si fa riferimento ad un programma del Client Access che viene messo a disposizione a partire dalla versione V5R1 ed è per questo motivo che nel paragrafo Prerequisiti ad inizio articolo ho fatto esplicito riferimento alla versione V5R1).
Una volta eseguito quanto suggerito nell’articolo IBM, non ho più avuto problemi.