Convert ANSI text file to UTF-8 with PowerShell

Years ago, to riduce the size of a Domino database, I scheduled a simple export task to a text file in CSV format.

Now, I need to import back those files in a RDBMS: PostgreSQL.

I didn’t defined the file format (damn…), so all the files are, by default, in ANSI code and US-ASCII charset.

Before importing these files in PostgreSQL I needed to do some substitutions in regexp, but both using Notepad++ or bash I always get wrong characters, even converting the file to UTF-8. After spending some too much time using Notepad++, TextPad and bash, I finally found a very simple solution using Windows PowerShell:

Get-Content archive-2011.csv | Out-File -Encoding UTF8 -filepath archive-2011-utf8.csv

That’s it!

For unknown reasons, I had some duplicated lines in my files. And, again, PowerSheel can do the job with in one line only:

gc archive-2011-utf8.csv | sort | get-unique > archive-2011-utf8-unique.csv

Coding the customizerBean in Dynamic View Panels

I used for the first time the Dynamic View Panel from the OpenNTF Extension Library. You can get started watching this video (oldie but goldie).

My need is to heavily customize the view rendered by the DVP, so the customizerBean is the way to go.

This is the sample code described in the above video. It only performs basic modifications, but is useful to understand the basis.
While, this Java class is a goldmine to gather lots of useful information.

I won’t explain how to set up the DVP and the customizerBean (watch the video if that’s what you’re looking for), what follows is just a collection of notes with code samples.

Using the Converter

If you want to change a column value you have to write your own converter class (extending the ViewColumnConverter) and set it to the column.
To work with properties in the converter class, you must serialize the parent class. Take in mind that the ColumnDef can’t be serialized anymore! So this (minimal) code won’t work:

public class DynamicViewCustomizer extends DominoViewCustomizer implements Serializable {
	private static final long serialVersionUID = -5126984721484501732L;

	private String panelId;

	@Override
	public IControl createColumn(final FacesContext context, final UIDynamicViewPanel panel, final int index, final ColumnDef colDef) {
		this.panelId = panel.getId();
		return super.createColumn(context, panel, index, colDef);
	}

	@Override
	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIComponent columnComponent = column.getComponent();
		DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;
		UIDynamicViewPanel panel = (UIDynamicViewPanel)FacesUtil.getComponentFor(context.getViewRoot(), this.panelId);
		dynamicColumn.setConverter(new ExtendedViewColumnConverter(null, colDef, panel));
	}

	public static class ExtendedViewColumnConverter extends ViewColumnConverter {
		private ColumnDef colDef;

		public ExtendedViewColumnConverter() {}

		public ExtendedViewColumnConverter(final ViewDef viewDef, final ColumnDef colDef, final UIDynamicViewPanel panel) {
			super(viewDef, colDef);
			this.colDef = colDef;
		}

		@Override
		public Object saveState(final FacesContext context) {
			Object[] superState = (Object[])super.saveState(context);
			Object[] state = new Object[2];
			state[0] = superState;
			state[1] = new Integer(this.colDef);
			return state;
		}

		@Override
		public void restoreState(final FacesContext context, final Object value) {
			Object[] state = (Object[])value;
			super.restoreState(context, state[0]);
			this.colDef= (ColumnDef)state[1];
		}
	}
}

While this will work fine:

public class DynamicViewCustomizer extends DominoViewCustomizer implements Serializable {
	private static final long serialVersionUID = -5126984721484501732L;

	private String panelId;

	@Override
	public IControl createColumn(final FacesContext context, final UIDynamicViewPanel panel, final int index, final ColumnDef colDef) {
		this.panelId = panel.getId();
		return super.createColumn(context, panel, index, colDef);
	}

	@Override
	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIComponent columnComponent = column.getComponent();
		DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;
		UIDynamicViewPanel panel = (UIDynamicViewPanel)FacesUtil.getComponentFor(context.getViewRoot(), this.panelId);
		dynamicColumn.setConverter(new ExtendedViewColumnConverter(null, colDef, panel));
	}

	public static class ExtendedViewColumnConverter extends ViewColumnConverter {
		private int numberAttrib;
		private int numberDigits;
		private int numberFmt;
		private int timeDateFmt;
		private String name;

		public ExtendedViewColumnConverter() {}

		public ExtendedViewColumnConverter(final ViewDef viewDef, final ColumnDef colDef, final UIDynamicViewPanel panel) {
			super(viewDef, colDef);
			// all the properties needed in the getAsString method
			this.numberAttrib = colDef.getNumberAttrib();
			this.numberDigits = colDef.getNumberDigits();
			this.numberFmt = colDef.getNumberFmt();
			this.timeDateFmt = colDef.getTimeDateFmt();
			this.name = colDef.getName();
		}

		@Override
		public Object saveState(final FacesContext context) {
			Object[] superState = (Object[])super.saveState(context);
			Object[] state = new Object[6];
			state[0] = superState;
			state[1] = new Integer(this.numberAttrib);
			state[2] = new Integer(this.numberDigits);
			state[3] = new Integer(this.numberFmt);
			state[4] = new Integer(this.timeDateFmt);
			state[5] = this.name;
			return state;
		}

		@Override
		public void restoreState(final FacesContext context, final Object value) {
			Object[] state = (Object[])value;
			super.restoreState(context, state[0]);
			this.numberAttrib = ((Integer)state[1]).intValue();
			this.numberDigits = ((Integer)state[2]).intValue();
			this.numberFmt = ((Integer)state[3]).intValue();
			this.timeDateFmt = ((Integer)state[4]).intValue();
			this.name = (String)state[5];
		}
	}

}

Or, even better, using a custom class, so that the saveState and restoreState methods will just have to save and restore one single object containing all the properties.

Convert icon columns

As shown on GitHub, you’d better use two converters: one for the icon columns and one for all the other types. Otherwise the converter will confuse itself, treating icon columns in categorized views as generic numeric/text columns.

The (minimal) code to arrange this:

public class MyViewBean extends DominoViewCustomizer implements Serializable {
	private static final long serialVersionUID = -5126984721484501732L;

	private String panelId;

	@Override
	public IControl createColumn(final FacesContext context, final UIDynamicViewPanel panel, final int index, final ColumnDef colDef) {
		this.panelId = panel.getId();
		return super.createColumn(context, panel, index, colDef);
	}

	@Override
	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIComponent columnComponent = column.getComponent();
		DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;
		UIDynamicViewPanel panel = (UIDynamicViewPanel)FacesUtil.getComponentFor(context.getViewRoot(), this.panelId);

		if (colDef.isIcon()) {
			dynamicColumn.setConverter(new MyIconColumnConverter(null, colDef, panel));
		} else {
			dynamicColumn.setConverter(new MyGenericColumnConverter(null, colDef, panel));
		}

	}
}

Replace icon columns with HTML

My XPages application uses Bootstrap, thus Glyphicons is the way to go. The icons in my Notes application are all custom image resources, so I just have to identify the name, change the column content to HTML and fill it with the code to the display the proper icon. Here a minimal example (top class is not Seralizable since MyIconColumnConverter doesn’t use properties):

public class MyViewBean extends DominoViewCustomizer {
	private String panelId;

	@Override
	public IControl createColumn(final FacesContext context, final UIDynamicViewPanel panel, final int index, final ColumnDef colDef) {
		this.panelId = panel.getId();
		return super.createColumn(context, panel, index, colDef);
	}

	@Override
	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIComponent columnComponent = column.getComponent();
		DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;
		UIDynamicViewPanel panel = (UIDynamicViewPanel)FacesUtil.getComponentFor(context.getViewRoot(), this.panelId);

		if (colDef.isIcon()) {
			dynamicColumn.setIconSrc(null);
			dynamicColumn.setValueBinding("iconSrc", null);
			dynamicColumn.setDisplayAs("");
			dynamicColumn.setContentType("html");
			dynamicColumn.setConverter(new MyIconColumnConverter(null, colDef, panel));
		}

	}

	public static class MyIconColumnConverter extends ViewColumnConverter {
		
		public IntranetIconConverter() {}

		public IntranetIconConverter(ViewDef viewDef, ColumnDef colDef, UIDynamicViewPanel panel) {
			super(viewDef, colDef);
		}
		
		@Override
		public String getAsString(final FacesContext context, final UIComponent component, final Object value) {
			
			String ret = "<span>&nbsp;</span>"; // default
			
			try {

				String glyphicon = null;
				if (value instanceof java.lang.String) {
					if (((String)value).compareToIgnoreCase("vwWorkInProgress.gif") == 0) {
						glyphicon = "dashboard";
					} else if (((String)value).compareToIgnoreCase("vwLinks.gif") == 0) {
						glyphicon = "link";
					} else if (((String)value).compareToIgnoreCase("vwEdit.gif") == 0) {
						glyphicon = "pencil";
					} else if (((String)value).compareToIgnoreCase("vwSent.gif") == 0) {
						glyphicon = "envelope";
					}
				}
				
				if (glyphicon != null)
					ret = "<span class=\"glyphicon glyphicon-" + glyphicon + "\"></span>";
				
			} catch (Exception e) {
				ret = "<span>N/A</span>";
			}
			
			return ret;
			
		}
		
	}
}

If you need to replace default Domino icons, just check if value instanceof Double and compare the numeric values (take a look at the IconColumnConverter class on GitHub).

Expand/collapse images

It is very easy to change expand/collapse images in a categorized view:

public class MyViewBean extends DominoViewCustomizer {

	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIComponent columnComponent = column.getComponent();
		DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;

		if (colDef.isCategorized()) {
			dynamicColumn.setCollapsedImage("/collapsed.gif");
			dynamicColumn.setExpandedImage("/expanded.gif");
		}

	}
}

Thanks to David Leedy and Paul T. Calhoun for the video and Jesse Gallagher for sharing his wonderful converter class on GitHub.

B-tree structure is invalid

I’m facing this problem when deleting a folder on three mail files.

The server is a Domino 9.0.1 FP4 64-bit and the database ODS is 52.

I’ve tryed almost everything, including:

load compact -C -D mail\db.nsf
load updall -R -C mail\db.nsf
load fixup mail\db.nsf
load convert mail\db.nsf * mail9.ntf -u

No luck. The problem is still there.
After googling something more, I found this post in the Notes/Domino 8 Forum that solved my problem.

So thank you very much Mike Hayman. I paste here the code as is, I only added the last line to remove the temporary document:

Dim sess As New notessession
Dim dbThis As NotesDatabase
Dim docTemp As NotesDocument
Dim vViews As Variant

Set dbThis=sess.CurrentDatabase
Set docTemp=dbThis.CreateDocument
docTemp.Form="Memo"
docTemp.Subject="0. TidyUpFolders"
Call docTemp.ComputeWithForm( False, False)
docTemp.Save True, False, True

vViews=dbThis.Views
Forall f_vw In vViews
If f_vw.IsFolder Then
Print f_vw.Name
Call docTemp.PutInFolder (f_vw.Name )
f_vw.Refresh
Call docTemp.RemoveFromFolder ( f_vw.Name)
f_vw.Refresh
End If
End Forall
Print "Complete!"

Call docTemp.RemovePermanently(True)

Bring NSF database back online after DBMT issues

If something goes wrong with DBMT you can experience problems accessing the database mainteined when the problem occurred:

The database is being taken off-line

The only way to recover the database is using the NSFBringDatabaseOnline C API.
Thus, create on the server a new database bringdbonline.nsf and an agent NSFBringDatabaseOnline with this code:

Option Public
Option Declare

Declare Private Function NSFBringDatabaseOnline Lib "nnotes" Alias "NSFBringDatabaseOnline" (  ByVal dbPath As String, ByVal Z As Long) As Integer

Sub Initialize
	Call NSFBringDatabaseOnline("offlinedb.nsf", 0)
End Sub

Agent’s trigger is On Schedule and the Runtime security level needs to be set to 3. Allow restricted operations with full administration rights.
Now simply run the agent from the Domino server console:

tell amgr run "bringdbonline.nsf" 'NSFBringDatabaseOnline'

Apache won’t start with a generic AH00016: Configuration Failed

The setup is a fresh installation of a Debian with Apache 2.4.10 and OpenSSL 1.0.1k, configured as a reverse proxy with RapidSSL wildcard certificate.

The apache2 service starts correctly but netstat -ant tells me that no service is listening on port 80 and 443. Disabling the SSL virtual host everything works fine. It’s not a syntax problem, since apachectl configtest returns “Syntax OK”.

So, after stopping apache2 and enabling the SSL virtual host, I started the webserver with the control interface:

apache2ctl -e info -k start

The action fails, and I got bounced to the error log. Here I only find these lines:

[Fri Aug 07 15:27:49.535978 2015] [ssl:info] [pid 11844:tid 140686718781312] AH01883: Init: Initialized OpenSSL library
AH00016: Configuration Failed

Fortunately, I have other Debian systems with the same version of Apache and OpenSSL working fine with a similar configuration, thus the problem could only be addressed to the certificate.

Is the certificate trusted by the CA?

# openssl verify -CAfile rapidssl-intermediate.crt rapidssl-certificate.crt
rapidssl-certificate.crt: OK

Positive response, no problem here.

Does the private key match the certificate?

# openssl x509 -noout -modulus -in rapidssl-certificate.crt
# openssl rsa -noout -modulus -in private.key

These commands should return the same value. If not, as in my case, you’re using the wrong private key!

@Usernameslist formula in XPages

@Usernameslist formula does not exists in SSJS, but you can write your own function to do the same job:

function @Usernameslist() {
	var server:lotus.notes.addins.DominoServer = new lotus.notes.addins.DominoServer(session.getServerName());
	var coll:java.util.Collection = server.getNamesList(session.getEffectiveUserName());
	return coll.toArray();
}

Thanks to Nathan T. Freeman.

Su FTSearch

Sto lavorando ad un progetto di migrazione su XPages di una vecchia applicazione di archiviazione documentale. C’è la presunzione di ottenere un risultato molto simile a quello di un motore di ricerca. Bing nello specifico, per il semplice fatto che graficamente è più accattivante dello scarno Google.

Oltre ad un edit box generico è prevista una ricerca avanzata per poter filtrare i risultati più precisamente agendo su campi specifici (categoria, cliente, autore, data, ecc.). C’è quindi una buona parte di logica dedicata alla produzione della query di ricerca che dovrà essere eseguita sull’indice full text per ottenere i risultati che rispettano i parametri di ricerca, presentati in uno stile comprensibile a lusers e power users:

Google

Ovvero titolo del documento in blu (che è un link che apre il documento), categoria di archiviazione in verde, data creazione ed eventuale data di ultima modifica in grigio scuro. Navigazione a pagine di dieci elementi ciascuna (tutto lavoro assegnato al repeat control). Pur avendo ottenuto un prototipo funzionante e appagante, ci sono un po’ di questioni da sottolineare.

Discordanza con le query eseguite nella search bar del client Notes

Per questa ho aperto una chiamata in IBM, perché ha davvero poco senso. Di primo acchito mi hanno suggerito di eseguire delle fuzzy search, ma è un inaffidabile palliativo.

Di fatto se un database (indicizzato) contiene un documento avente in un campo il valore This is a document, il client sarà in grado di trovarlo ricercando this document. Mentre il metodo FTSearch della NotesDatabase e della NotesView no. E nemmeno la proprietà search della view data source di una XPage. In questi casi la stringa viene trattata come se fosse scritta tra doppi apici, cioè come se le due parole fossero consecutive. Confido in un parametro non documentato che permetta di uniformare le cose, perché altrimenti è poco serio che, a parità di query e di database, il client restituisca risultati diversi rispetto alla XPage. Ed è anche sbagliato che vada io a inserire un AND in sostituzione ad ogni spazio dato che potrei potenzialmente invalidare una query complessa che alcuni utenti sono effettivamente in grado di fare.

Ordinamento

Che poi è strettamente collegato al punto precedente. Perché le ricerche eseguite da codice non possono essere ordinate come quelle eseguite dalla search bar? L’ottimo sarebbe poter eseguire una ricerca su una vista ordinata nello stesso modo in cui si vuole presentare i risultati, sfruttando quella che nel client è la modalità Sorted like current view. Dunque l’algoritmo di ricerca Domino lo prevede, perché non è stato reso disponibile anche via codice?

Ciascun documento archiviato dispone di due campi: data di creazione e data di ultima modifica. I risultati delle ricerche eseguite via XPage dovranno porre in testa i documenti più recenti, usando cioè la data di modifica (se presente) o in alternativa la data di creazione. Il metodo FTSearch della NotesDatabase prevede l’ordinamento ascendente o discendente per data di creazione (che è la proprietà del documento), quindi ordinare i risultati a proprio piacimento non è di fatto possibile!

L’omonimo metodo della NotesView dice invece che:

If the database is not full-text indexed, the documents in the subset are in the same order as they are in the original view. However, if the database is full-text indexed, the documents in the subset are sorted into descending order of relevance.

Quindi, senza indice full text i risultati sarebbero nello stesso ordine in cui si presentano nella vista. Mentre disponendo dell’indice sarebbero ordinati per rilevanza.

Fortunatamente esiste un workaround, che può funzionare solamente agendo sulla ricerca dalla NotesDatabase dato che nella NotesView non è prevista alcuna opzione sull’ordinamento dei risultati. Creando il campo $Created all’interno di ciascun documento, calcolato come segue:

@If(DataModifica = ""; DataCreazione; DataModifica)

E applicando il metodo di ordinamento FT_DATECREATED_DESC si ottiene un elenco ordinato in base al valore del campo $Created che sostituisce l’effettiva data di creazione del documento.

Ma se l’esigenza fosse stata quella di ordinare i risultati per un campo di tipo testo non ci sarebbe stata altra scelta che il solito PutAllInFolder che avrebbe complicato le cose e rallentato il processo di ricerca, che senza inutili spostamenti in cartelle temporanee è invece fulmineo pur trattando centinaia di migliaia di documenti.

Occhio alla documentazione

Lo farò presente al supporto (una cosa alla volta, ho troppe chiamate aperte contemporaneamente credo di essere a rischio blacklist): sul metodo FTSearch della NotesDatabase (JavaScript) c’è una effe di troppo sul parametro FFT_DATECREATED_DES:

FFT_DATECREATED_DES

Riscrivere endpoint dei web service Domino dietro reverse proxy HTTPS

Un limite noto con cui convivevo da anni. Per riprodurre il problema è sufficiente un server Domino e un reverse proxy. Il reverse proxy dispone di certificato SSL, quindi il client si collega al reverse proxy in HTTPS mentre il reverse proxy a Domino in HTTP.

Il problema consiste che nei web service esiste l’endpoint, ovvero l’indirizzo a cui il client dovrà collegarsi per consumare il web service. Tale indirizzo è presente nell’attributo location dell’elemento wsdlsoap:address, che Domino genera dinamicamente in base alla richiesta HTTP che viene fatta al server, tenendo cioè conto del protocollo (HTTP/HTTPS) e dell’hostname.

Quando il client si collega al web service, Domino vedrà protocollo e hostname del reverse proxy, generando un endpoint potenzialmente errato. L’hostname è facilmente correggibile tramite il DNS interno, facendo in modo che l’hostname pubblico venga risolto dal DNS interno nell’indirizzo interno del server Domino. Ma per mantenere il protocollo non c’è alternativa che far collegare il reverse proxy a Domino in SSL.

Dopo aver interpellato anche IBM, e ottenuto una APAR a cui partecipiamo io e ben due altri clienti, mi viene confermato che l’unica possibile soluzione è installare un certificato SSL anche sul server web di Domino. Ma io non voglio installare il certificato anche su Domino!

Il reverse proxy è un server Apache che dispone del modulo mod_substitute, in grado di manipolare il contenuto delle risposte che attraversano il proxy.

L’implementazione è semplice, purché ci si accorga subito che è necessario disattivare la compressione gzip altrimenti il mod_substitute tenterebbe di manipolare la risposta già compressa che è, chiaramente, indecifrabile.

Supponendo il caso che il client si collega al reverse proxy all’indirizzo https://server.acme.com/app/ordini.nsf/elenco?WSDL e il reverse proxy a Domino all’indirizzo http://server.acme.com/app/ordini.nsf/elenco?WSDL, per correggere il protocollo sarà necessario modificare la configurazione del Virtual Host gestito da Apache come segue:

<Location /app/>
	RequestHeader unset Accept-Encoding
	AddOutputFilterByType SUBSTITUTE text/xml
	Substitute "s|http://server.acme.com:80|https://server.acme.com|ni"
</Location>

Ho applicato la regola all’intera directory /app/ dato che ho più applicazioni che dovranno essere soggette alla sostituzione, ma nel caso fosse stata solamente una avrei limitato la sostituzione a quella specifica applicazione specificandone il percorso esatto nella Location.

Di nuovo online

Anno nuovo blog nuovo. Letteralmente: mi sono trasferito presso altro provider e dopo tanti anni sono tornato a WordPress. Che devo dire ha fatto passi da gigante rispetto al WordPress del 2007 che mi ricordavo io. Per il tema sono partito da Underscores che con un po’ di pazienza ho personalizzato grazie alle sempre utili palette di Colour Lovers.

Il 2014 è stato lavorativamente l’ennesimo anno di crescita. Ma a parte questo è stato un anno tremendo. Motivo per cui gli scorsi 12 mesi ero, di fatto, uscito di scena.

Ma con il 2015 ho deciso di riprendere in mano il blog. Principalmente perché spesso, dopo aver trovato la soluzione a quel problema su cui stai sbattendo la testa da ore o per quelle poche righe di codice che magari sono una banalità ma ci hai sudato sopra per farle funzionare, la voglia di scriverci sopra due righe era tanta. Il blog mi mancava.

Ecco che allora in mezza giornata il tema era pronto e finito (il più del tempo è andato perso per trovare la giusta palette). In un’altra mezza giornata ho scritto un agente che migrasse tutti i dati dal vecchio blog NSF (immagini comprese!). Ed eccomi qua, a combattere con gli ultimi postumi dell’influenza, senza la quale però dubito che sarei riuscito a ritagliarmi il tempo necessario per questi lavoretti.

Terminato il pistolotto veniamo al sodo.
I temi su cui continuerò a scrivere saranno grossomodo sempre gli stessi. Lavoro ancora con IBM Lotus Notes/Domino. Sempre di più con le XPages, anche se non sono ancora ai livelli che vorrei. Lo sviluppo web rientra sempre nelle mie passioni, anche se l’ho un poco snobbato negli ultimi periodi. Di hardware, virtualizzazioni e attività sistemistiche non me ne occupo praticamente più ormai. Con GNU/Linux ci lavoro ancora, poco però.
Continuano a piacermi la musica, le serie tv e le auto. Il tempo libero continua ad essere assorbito da Baxter (che a breve festeggerà i suoi sette anni) e dalla fotografia, attività a cui mi sono avvicinato dalla scorsa estate, che mi sta appassionando ma di cui sono ancora molto ignorante.

Svuotare l’elenco applicazioni nel Designer

Chi come me lavora con diversi Clienti e su diversi progetti si trova presto ad avere la lista delle applicazioni piena al punto che è inutilizzabile.
Eliminarle una alla volta è una seccatura, a maggior ragione se si riferiscono ad applicazioni on server a cui non si ha accesso nel preciso momento in cui si esegue l’operazione.

Una funzione Clear sarebbe stata apprezzata!

È tuttavia fattibile ma in modo manuale, svuotando cioè il contenuto del file wsInfo.xml contenuto nella directory workspace\.metadata\.plugins\com.ibm.designer.domino.ide.resources all’interno della data directory.

N.B. Non è possibile farlo a caldo, cioè con il Client aperto. Chiudere, dunque, Notes, modificare il file wsInfo.xml e quindi riaprirlo.