Adicionar novas línguas ao Windows (edições Home)

July 2nd, 2010

Eu desenvolvo sobre Windows, sobre um Windows 7 Home Premium, para ser mais exacto. E ao longo do tempo tenho sofrido de alguns obstáculos oferecido pelas limitações da versão. A versão é a que veio com o PC, e pretendo puxá-lo até onde possível, antes de fazer um upgrade para um Business ou Ultimate (apesar das restantes máquinas – desktop – aqui do escritório estarem com o Ultimate).

Mas infelizmente ontem, deparei-me com um problema que demorou a tarde toda a perceber, e quando assim é, é geralmente algo do OS e nada óbvio. Contextualizando, estou numa fase de internacionalizar uma aplicação web de gestão de currículos e processos de recrutamento e selecção, que desenvolvi no ano passado (e ainda este ano). Foi grande sucesso junto da equipa de trabalho, pois permitiu acelerar em muito os processos de recrutamento, que dado o volume de candidaturas que lhes chegavam – dezenas de milhares em meio ano – era tediante e constrangedor. Felizmente, a app foi criada à medida da equipa e adaptado ao processo de trabalho, e resultou em pleno.

De qualquer forma, a app está a ser internacionalizado para suportar as acções em Espanha, e o processo de internacionalização tem os seus próprios desafios associados – a globalização da aplicação para não ter dependências directas da língua nem numeração, e a localização para corresponder a cada realidade, quer de linguagem, quer de informação (por exemplo as diferenças de moradas e códigos postais e informação regional).

Sei que esta fase trará alguns problemas de estrutura e configuração – umas mais complicadas que outras – e a primeira apareceu logo na base de dados. Para a aplicação, optei pelo suporte de dados em base de dados relacional, e a base escolhida foi o PostgreSQL. Tem óptimas capacidades de armazenamento, suporte a pesquisa em XML, indexação e pesquisa de texto livre com o tSearch2, e é OpenSource que também foi muito atractivo na escolha.

Quando se cria uma base de dados no Postgre, o único parâmetro obrigatório é o nome da base de dados. As restantes definições são preenchidas com os valores pré-definidos da instalação. As que condicionam a base e que só podem ser definidas na criação são o LC_COLLATE e o LC_CTYPE. A primeira – LC_COLLATE, determina a forma como são ordenadas as palavras e que pode variar de cultura para cultura. O mesmo para o LC_CTYPE que caracteriza os caracteres em termos de maiúsculas, minúsculas, e pontuação

O problema destes parâmetros são a dependência do OS para esta informação. Seria muito interessante se dependesse apenas da base de dados e os executáveis associados, mas na verdade esta informação vem do sistema operativo. Por defeito, o valor para as variáveis são o da definições do OS. O Windows Server 2008 permite que o utilizador possa configurar a sua conta para definições regionais e linguísticas diferentes e que podem ser acrescentados através de MUIs (Multilingual User Interface). O mesmo é possível nas versões do Business e Ultimate do Windows. Infelizmente, no Home Premium não é possível, sem alguns workarounds.

Como afecta a criação da base? Simples. Se não tiver as definições linguísticas instaladas na máquina, não posso criar a base de dados usando essas definições, apenas com as existentes. Para portuguese, e portanto com base na definição do OS, tenho quer para LC_CTYPE, quer para LC_COLLATE o valor:

Portuguese_Portugal.1252

Para a base espanhola queria

spanish_Spain.1252

Acrescentar definição no Windows Server 2008

Trabalhamos aqui geralmente com duas instâncias de base de dados, uma local e uma remota, centralizada. Procurei resolver primeiro o caso no server, para verificar se era mesmo este o problema. Não tinha o pack espanhol instalado, e portanto puxei-o do site da Microsoft:

http://www.microsoft.com/Downloads/details.aspx?familyid=03831393-EEF7-48A5-A69F-0CE72B883DF2&displaylang=en

ou para o Server 2008 c/ SP2

http://www.microsoft.com/Downloads/details.aspx?familyid=3A7FB7A2-3519-495B-9BC5-2007082CA9A6&displaylang=en

ou para Server 2008 R2

http://www.microsoft.com/Downloads/details.aspx?familyid=03831393-EEF7-48A5-A69F-0CE72B883DF2&displaylang=en

são ficheiros .img pelo que devem ser carregados numa drive virtual como o Virtual Clone Drive.

Depois em Control Panel > Regional and Language Options > Keyboards and Languages > Display Language aparece a lista de linguagens disponíveis e em uso pelo Windows. Há tb um botão “Install/uninstall Languages” que permite acrescentar. Deve no Wizard que aparece escolher a pasta da drive para a linguagem especifica e deixar correr a instalação.

No caso do espanhol, puxei o ficheiro do grupo 1, e no wizard escolhi a pasta “es-ES”. “Et voilá”, spanish_Spain.1252 fica disponível no PostgreSQL. Simples e resolvido.

O processo para as versões do 7 Ultimate e Business devem ser semelhantes. O problema surge para o Home. Não é suportado, mas é possível introduzir as definições de localização. Para tal guiei-me pelas notas em http://xaueious.wordpress.com/2009/08/22/changing-installed-language-of-windows-7-home-premium-pro-from-en-us/ e segui o caminho da linha de comandos.

Acrescentar definição no Windows 7 Home Premium

  1. Descarregue o MUI associado à língua específica daqui http://www.froggie.sk/7lp64rtm.html
  2. Agora um dos truques: Se executares o ficheiro descarregado, é extraído um ficheiro “lp.cab” para a mesma pasta e que é fundamental preservar. No entanto, pouco depois de executar, o ficheiro desaparece. Portanto considera-o um pouco como um jogo de reacção, e prepara o ambiente para mover o ficheiro para outro local ou renomea-lo ASAP. Copy-paste não resulta.
  3. Na linha de comandos, executado com privilégios de administrador, executa o seguinte comando:
    DISM /Online /Add-Package /PackagePath:D:\Caminho\lp.cab

    onde D:\Caminho\lp.cab é o caminho completo para o ficheiro extraído.

  4. O processo demora uns minutos, mas uma vez findado, no Postgre, é possível criar a base com as chaves de locale correctas e desejadas.

PostgreSQL e Linux

December 5th, 2009

Cada vez mais aprecio o PostgreSQL. A base de dados é, efectivamente, muito capaz e poderosa, e felizmente não transporta a barreira das licenças que alguns outros sistemas de base de dados portam. Não é que não os justificam, e empresas que compram licenças desses sistemas reconhecem a sua importância e valor. Mas o PostgreSQL é efectivamente uma base de dados bastante simpático e funcional com um custo reduzido.

Algo que considero muito útil no Postgre é ser multi plataforma. Windows, Linux e MAC.. é escolher, que o PostgreSQL corre. O PgAdminIII, aplicação de gestão da base de dados com GUI também corre em Windows e Linux. Ainda não testei comunicação entre base de dados e servers aplicacionais suportando SOs diferentes, mas penso que é evidente o correcto funcionamento e comunicação.

Hoje estou a instalar um server em Linux, para suportar o JIRA.Tive dificuldades com o Confluence (coisa estranha de má tradução entre IIS e Tomcat dos endereços das páginas), e decidi mover as aplicações de gestão para um server dedicado. O Ubuntu é a minha “flavor” preferida (é todo o conceito…). As aplicações da Atlassian utilizam Tomcat, e tem versões que portam o serviço com elas. Pensei em o server como Tomcat server (o Ubuntu tem essa opção), mas decidi seguir a recomendação do fabricante e deixar as aplicações correr em instâncias dedicadas. O que instalei por defeito foi o clássico LAMP e a base de dados PostgreSQL.

No entanto, o PG n funciona por si só sem uma ligeira configuração. No Windows, é realizado na instalação com o Wizard, mas no Linux é mais simples / imediato com umas linhas de comando. Naturalmente o processo está mais que documento na web, mas nunca é demais reescrever.:

Para começar, duas instruções para instalar o PostgreSQL (caso n tenha sido usado a opção de instalação no processo de instalação do SO):

sudo apt-get install postgresql
sudo apt-get install pgadmin3

O primeiro pode ser ignorado caso a instalação tenha sido efectuada na instalação do SO. A segunda é um GUI de administração muito útil.

Segue então a configuração, sendo necessário inicializar o utilizador base e a password para o mesmo:
sudo -u postgres psql postgres
\password postgres

e introduza a password desejada para o utilizador postgres.

Outras coisas que podem ser feitas:
criar uma bd:> sudo -u postgres createdb [nome da base]
iniciar o serviço:> sudo /etc/init.d/postgresql-8.4 [start | stop | restart]

Finalmente, há mais dois detalhes importantes a resolver. O Postgre é, por defeito, bastante restriivo no acesso, permitindo acesso apenas por conexões vindas da própria máquina. Para aceder remotamente, primeiro deve permitir que os utilizadores da base possam autenticar-se na base na rede (caso queira esta funcionalidade). É necessário acrescentar o seguinte ao ficheiro /etc/postgresql/8.4/main/pg_hba.conf


# TYPE DATABASE USER IP-ADDRESS IP-MASK METHOD
host all all x.x.x.0 255.255.255.0 md5

onde x.x.x.0 é a definição da rede (p.e. 10.0.0.0 ou 192.168.0.0).

Para que seja possível acesso externo ao servidor de base de dados, e necessário editar /etc/postgresql/8.4/main/postgresql.conf, retirando o comentário à linha #listen_addresses = ‘localhost’
e sustituír ou acrescentar ao ‘localhost’ o ‘*’ para todas as conecções, ou uma gama de IPs para limitar. P.e.:
listen_addresses = ‘*,localhost’

E já vai bem encaminhado :D

Concatenação de resultados em PostgreSQL

April 6th, 2009

Hoje enquanto tentava solucionar uma funcionalidade de uma aplicação que estou a desenvolver, surgiu a necessidade de encontrar forma de concatenar, em string, os valores retornados por várias linhas de uma pesquisa. A ideia era poder juntar num só campo, um vector léxico (tsvector do PostgreSQL) os campos texto de várias tabelas referentes à mesma entidade, para permitir uma pesquisa por texto livre centralizada.

Por exemplo supomos uma tabela simples “pessoa” com campos id_pessoa, nome e localidade (os últimos dois são varchars). Supomos também uma segunda tabela “notas”, com campos id_nota, id_pessoa, e texto, onde id_pessoa é chave externa e texto é do tipo text. Assumimos ainda que o campo notas pode ter mais que um registo por pessoa, como também pode não existir nota. A minha intenção é que consiga concatenar os vários campos de texto e vectorizar esse texto, para pesquisar nele usando o TSearch2 do PostgreSQL.

O PostgreSQL tem algumas funções úteis para isto, nomeadamente to_tsvector, COALESCE, array_to_string, array e o operador de concatenação ||.

Analisando primeiro pessoa, para concatenar o texto, teria qualquer coisa do género:

SELECT nome || ' ' || localidade FROM pessoa

o operador || efectua a concatenação do texto das colunas (que são dum tipo que represente texto). A operação anterior é efectuada para cada linha da tabela. Para vectorizar, aplico o to_tsvector:

SELECT to_tsvector((SELECT nome || ' ' || localidade FROM pessoa WHERE id_pessoa = xxx))

O to_tsvector vectoriza a string léxicamente, mas exige que a entrada seja um campo (texto) apenas, daí indicar um WHERE clause que garante um campo apenas. O segundo par de parenteses também é necessário, em torno da subquery. Teria que executar a query num ciclo qualquer para poder manipular várias linhas, individualmente.

Imagina que no campo nome tinha “Miguel da Silva Alho” e em localidade tinha “actualmente em Murtosa, Portugal”, o vector resultante seria:

'alho':4 'silv':3 'actual':5 'miguel':1 'murtos':7 'portugal':8

com a eliminação das palavras de paragem e a redução às raizes das palavras exemplificado por “actual”, “silv” e “murtos”.

Para efectuar o mesmo para as notas, a operação é semelhante, mas necessito de ter o cuidado e evitar um retorno nulo. Para tal aproveito a função COALESCE presente na maioria dos sistemas de base de dados. O COALESCE permite analisar diversos valores, retornando o primeiro não nulo.

Se tivese a certeza que apenas tinha uma linha por pessoa, seria:

SELECT to_tsvector(COALESCE(SELECT texto FROM notas WHERE id_pessoa = xxx), '')

Com COALESCE, se o SELECT retornar um valor válido, é efectivamente esse que é vectorizado. Se no entanto o resultado do SELECT for nulo, é testado a nulidade do valor seguinte, neste caso uma string vazia (mas não nula). Como é não nulo, este é vectorizado (como se fosse to_tsvector(”) ). O truque surge no entanto, se quero juntar o texto das diversas notas de uma pessoa. Nesse caso necessito de recorrer ao array_to_string() e fico com :

SELECT to_tsvector(COALESCE((
	SELECT array_to_string(
	  array(SELECT texto FROM notas WHERE id_pessoa = xxx), ' ')
	),''))

Em array, a lista de resultados é convertido para um array, e com o array_to_string, cada item do array é concatenado, separado apenas pro um espaço (como definido no segundo parâmetro da função). Novamente , tenho um COALESCE para prevenir um valor null do conjunto.

Tendo agora os dois vectores, posso concatena-los:

SELECT	to_tsvector(
		(SELECT nome || ' ' || localidade FROM pessoa WHERE id_pessoa = xxx)
	) || 
	to_tsvector(COALESCE(
               (SELECT array_to_string(
	  	  array(SELECT texto FROM notas WHERE id_pessoa = xxx), ' ')
		),'')
       )

Repara que tenho a concatenação dos vectores através de ||. se o segundo fosse nullo, o resultado seria tb nullo, independentemente do resultado do primeiro vector. Daí a importância do COALESCE.

E aí está.

Backup de PostgresSQL em C#

March 25th, 2009

Para uma aplicação que estou a desenvolver com base numa BD Postgre, precisei de criar um script de backup da base de dados. A ideia é clicar num botão da interface web, e fazer o dump da BD, de forma simples, e permitir que o utilizador (que neste caso não tem nada a haver com IT) possa descarregar e archivar a base facilmente.

O método:

/// <summary>
        /// Backup Database to file (dump)
        /// </summary>
        /// <param name="server"> </param>db -> ConfigKey ["dbbackupserver"]
        /// <param name="port"> </param>db -> ConfigKey ["dbbackupport"]
        /// <param name="user"> </param>db -> ConfigKey ["dbbackupuser"]
        /// <param name="password"> </param>db -> ConfigKey ["dbpbackuppassword"]
        /// <param name="dbname"> </param>db -> ConfigKey ["dbbackupdbname"]
        /// <param name="backupdir"> </param>db -> ConfigKey ["dbbackupdir"]
        /// <param name="backupFileName"> </param>db -> ConfigKey ["dbbackupfilename"]
        /// <param name="backupCommandDir"> </param>db -> ConfigKey ["dbbackupworkdir"]
        /// <returns>The file name with the db dump</returns>
        public string BackupDatabase(
            string server, 
            string port, 
            string user, 
            string password,
            string dbname, 
            string backupdir, 
            string backupFileName,
            string backupCommandDir)
        {
            //string password = ConfigurationManager.AppSettings["dbpbackuppassword"];
            //string server = ConfigurationManager.AppSettings["dbbackupserver"];
            //string port = ConfigurationManager.AppSettings["dbbackupport"];
            //string user = ConfigurationManager.AppSettings["dbbackupuser"];
            //string dbname = ConfigurationManager.AppSettings["dbbackupdbname"];
            //string backupdir = ConfigurationManager.AppSettings["dbbackupdir"];
            //string backupFileName = ConfigurationManager.AppSettings["dbbackupfilename"];
            //string backupCommandDir = ConfigurationManager.AppSettings["dbbackupworkdir"];
 
            try
            {
 
                Environment.SetEnvironmentVariable("PGPASSWORD", password);
 
                string backupFile = backupdir + backupFileName +
                    DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".backup";
                string BackupString = "-ibv -Z3 -f \"" + backupFile + "\" " +
                    "-Fc -h " + server + " -U " + user + " -p " + port + " " + dbname;
 
                Process proc = new System.Diagnostics.Process();
                proc.StartInfo.FileName = backupCommandDir + "\\pg_dump.exe";
                proc.StartInfo.Arguments = BackupString;
 
                proc.Start();
 
                proc.WaitForExit();
                proc.Close();
 
                return backupFile; 
 
            }
            catch (Exception ex)
            {
                throw new Exception("An unknown error occured while trying to perform the database backup/restore operation.\n\nException: " + ex.Message);
            }
        }

Basicamente, o método inicia o processo / commando da shell “pgdump” que vem com a instalação do PostgreSQL e faz um dump das tabelas e dados. Eu passo os parâmetros de nome de ficheiro e BD e directório de armazenamento etc, como parâmetro do método, porque tenho integrado numa framework e deverá servir para outras aplicações, mas podia perfeitamente referenciar as chaves da configuração directamente ou mesmo escrever o código do commando (hardcoded). No fim, ele devolve o caminho do ficheiro para enviar para a interface.