Indice
Autores
Jose María Rodríguez Valls
webmaster@elcurriculum.com
- Documentación y Presentación
- Módulos de Login y Chat
- Pruebas modulares
Guillem Rull
e7723349@est.fib.upc.edu
- SSL
- Encriptación de mensajes entre clientes
- Pruebas modulares
Bernat Requesens Fernandez
e4023490@est.fib.upc.edu
- Conexión/Desconexión con servidor
- Envio y Recepción de mensajes
- Pruebas modulares
Jordi Valls Pérez
e4987267@est.fib.upc.edu
- Integración de los todos los módulos
- Creación de los módulos de agregar contacto y main
- Pruebas globales
Requisitos del Sistema
- Maquína virtual de Java
- Conexión a internet
- Puertos 5222 o 5223 abiertos
- Se requiere alguna cuenta en los diferentes servidores jabber
Introducción
El Proyecto PXCJabber es un cliente de mensajería instantanea basada en el protocolo de comunicación Jabber.
Jabber utiliza el XML como medio para comunicarse con los diferentes servidores. Se disponen de las librerias de comunicación JSO que permiten interactuar con el servidor en dicho protocolo.
Hemos conseguido conectarnos con el servidor, obtener la lista de usuarios, abrir una conversación con otro cliente y la posibilidad de encriptar los mensajes.
¿Que es Jabber?
JABBER es un protocolo de comunicación para la mensajería instantanea.
Características:
- Abierto: El protocolo jabber es gratuito, abierto, publico y comprensible.
- Extensible: Usando XML cualquiera puede implementar nuevas capacidades.
- Seguro: Cualquier servidor puede ser aislado de la comunidad pública de JABBER.
- Descentralizado: Cualquiera puede montar su propio servidor JABBER.
¿Que queremos?
Pretendemos desarrollar un cliente de mensajería instantanea en entorno gráfico utilizando el protocolo JABBER.
El objetivo es implementar diversas funcionalidades tipicas de este tipo de clientes. En ningún momento pretendemos aportar nada nuevo.
Solo experimentaremos con dicho protocolo.
¿Como ejecutar el Cliente?
Estructura del proyecto
Los archivos principales del proyecto (los que hemos implementado nosotros) estan dentro de la carpeta principal. Las imagenes y iconos utilizados estan todos dentro de la carpeta de imagenes.
Se pueden personalizar y agregar smileys sin ningún tipo de problema. PXCJabber soporta gif´s animados y jpgs.
Las carpetas com, META-INF, net y org pertenecen a la JSO utilizada para la comunicación. No están implementadas por nosotros.
Ejecución
Para la ejecucción necesitaremos disponer de un JDK a ser posible la última versión JDK 1.5.0
Para evitar tener que incluir el archivo jso-full.jar (librerias JSO) en el CLASSPATH lo hemos descomprimido directamente en la carpeta del proyecto. Esto nos permite la posibilidad de compilar directamente con un javac *.java
Si deseamos ejecutar el cliente podemos ejecutar un java Main
Hemos creado varios usuarios de prueba:
- USUARIO: pxc5 PASSWORD: pxcpxc HOST: jabber.org
- USUARIO: pxc3 PASSWORD: pxcpxc HOST: jabber.org
- USUARIO: ss PASSWORD: ss HOST: jabber.org
Comparativas
Clientes Existentes
En Internet existen muchos tipos de clientes de mensajería instantanea que utilizan diversos protocolos.
El más conocido sin ningún tipo de duda es el MSN Messenger que dispone de la mayor cantidad de usuarios. Yahoo le sigue de cerca con su propio cliente de mensajeria instantanea.
En la actualidad existen diferentes tipos de clientes de mensajeria que permiten utilizar diferentes protocolos de comunicación. Esto nos permite hablar con diferentes contactos independientemente del protocolo.
Jabber es un protocolo poco conocido que esta abriendo su propia cuota de usuarios.
MSN vs Jabber
Principales Diferencias:
- MSN Messenger utiliza un protocolo propio, existen diferentes versiones del protocolo de MSN.
Actualmente usa MSNP10 pero también exiten las versiones: MSNP9 y MSNC1
Jabber tiene un protocolo formado por una única versión y ha sufrido pequeñas modificaciones.
- El protocolo Jabber es mucho más manejable por ser XML puesto que permite el parseado y la manipulación de los datos de una manera sencilla y rapida.
- MSNP9 es un protocolo más rapido que jabber. Se envian menos caracteres entre el cliente y el servidor cosa que aumenta la velocidad de transferencia.
- Jabber realiza una conexión directa con el servidor. MSNP9 realiza una conexión con un servidor representante llamado Dispatch Server. Dicho servidor nos retorna la ip de la máquina que nos hará de servidor. Esto permite distribuir la carga de comunicación entre diferentes máquinas.
- Jabber esta bajo licencia GNU cosa que facilita mucha documentación y ejemplos tanto de servidores como de clientes. Es dificil encontrar documentación referente al funcionamiento de MSNP9.
Modulo de SSL
Descripción
Este modulo ofrece una función para crear e inicializar una conexión SSL entre nuestro cliente y el servidor jabber.
Esto implica que cuando enviemos un mesaje, la información viajará cifrada hasta el servidor pero no necesariamente entre el servidor y el cliente destinatario (o entre servidores, si el destinatario tiene su cuenta en otro servidor), si no es que este ultimo también usa SSL para conectarse con el servidor.
Implementación
ModulSSL.java
Veamos la signatura publica de esta clase:
class ModulSSL {
public static StartTLSSocketStreamSource crearSSL(
JID servidor, int port) throws IOException,
CouldNotConfigureSSLException
}
El metodo 'crearSSL' es el unico que necesitan los otros modulos del proyecto para interactuar con esta clase. Este metodo crea un socket SSL y lo conecta con el servidor indicado en el puerto indicado (normalmente el 5223, segun el protocolo jabber), y lo devuelve encapsulado en un objeto 'StartTLSSocketStreamSource' que es lo que requieren las JSO para comunicarse con el servidor.
En cuanto a las excepciones, puede lanzar una 'IOException' si se produce un error al crear el socket, así como una 'CouldNotConfigureSSLException' si hay algun problema al inicializar el SSL.
Una vez creado e inicializado el socket SSL, se utiliza de la misma forma que si fuera un socket normal. El login, envio de mensajes y demás, se hace todo igual, pues ya se encarga el socket de hacer la encriptación de manera transparente.
Veamos como se inicializa el socket SSL:
private static SSLContext obtainContext()
throws CouldNotConfigureSSLException {
SSLContext ctx = null;
try {
ctx = SSLContext.getInstance("SSL");
ctx.init(null, new TrustManager[]
{new TrustEverythingManager()}, new SecureRandom());
} catch (Exception e) {
throw (new CouldNotConfigureSSLException());
}
return ctx;
}
Para ello usamos este metodo privado, donde básicamente lo unico que hay para destacar es el TrustManager, que se encarga de validar el certificado del servidor. Aquí es donde surge el problema, pues en la mayoria de los casos los servidores jabber tienen un certificado firmado por ellos mismos, es decir, que no hay una entidad certificadora que les haya emitido un certificado sino que se lo han hecho ellos mismos.
Esto provoca que para verificar el certificado del servidor debemos confiar en el como entidad certificadora. Para poder hacer esto, hemos de crear nuestro propio TrustManager para que acepte como valido cualquier certificado.
TrustEverythingManager.java
class TrustEverythingManager implements X509TrustManager {
public TrustEverythingManager() {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs,
String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] certs,
String authType) throws CertificateException {
}
}
Lo importante de esta clase es que 'checkClientTrusted' y 'checkServerTrusted' no generan nunca una 'CertificateException', es decir que siempre aceptan el certificado.
Observaciones
Se han invertido unas 5 horas en documentación sobre el SSL en java, unas 6 horas en programación y pruebas (unas 3 de programacion y otras 3 de pruebas), y unas 2 horas en documentar el modulo.
La parte mas complicada sin duda ha sido resolver el problema del TrustManager y llegar a la conclusión de que habia de hacerse uno propio.
Pruebas
Una primera prueba ha sido hacer un pequeño programita que se conectara al servidor y hicera login usando SSL.
class ProvaSSL {
public static void main(String args[]) throws Exception {
JSOImplementation jso = JSOImplementation.getInstance();
Stream conn = jso.createStream(Utilities.CLIENT_NAMESPACE);
StreamDataFactory sdf = conn.getDataFactory();
JID cliente = sdf.createJID("grf@jabber.org/PXCJabber");
JID servidor = sdf.createJID(cliente.getDomain());
//establim la conexio SSL amb el servidor
conn.connect(ModulSSL.crearSSL(servidor, 5223));
System.out.println("conectat");
conn.open();
System.out.println("obert");
//fem login
AuthFeatureConsumer au = new AuthFeatureConsumer(servidor,
cliente.getNode(), "grf", cliente.getResource());
au.authenticate(conn, conn);
//Obtenim la llista de contactes
InfoQuery iq = (InfoQuery)sdf.createPacketNode(
sdf.createNSI("iq", conn.getDefaultNamespace()),
InfoQuery.class);
iq.setType(InfoQuery.GET);
IdentityGenerator idgen = new IdentityGenerator();
iq.setID(idgen.generate("roster"));
RosterQuery rosterq = (RosterQuery)sdf.createExtensionNode(
sdf.createNSI("query", RosterQuery.NAMESPACE),
RosterQuery.class);
iq.add(rosterq);
iq = (InfoQuery)PacketMonitor.sendAndWatch(conn, iq);
if ((iq == null) || (iq.getType() != InfoQuery.RESULT)) {
System.out.println("No s'ha pogut obtenir la
llista de contactes");
return;
}
rosterq = (RosterQuery)iq.getExtension(RosterQuery.class);
List contactes = rosterq.listElements(RosterItem.class);
Iterator iter = contactes.iterator();
System.out.println("Roster:");
while (iter.hasNext()) {
RosterItem contacte = (RosterItem)iter.next();
System.out.println("\t" + contacte.getJID());
}
//Notifiquem el nostre estat
Presence pre = (Presence)sdf.createPacketNode(
sdf.createNSI("presence", conn.getDefaultNamespace()));
pre.setStatus("I am available");
conn.send(pre);
//Esperem un segon
Thread.currentThread().sleep(1000);
//Cambien l'estat per indicar que ens n'anem
pre = (Presence)sdf.createPacketNode(sdf.createNSI(
"presence", conn.getDefaultNamespace()));
pre.setType(Presence.UNAVAILABLE);
pre.setStatus("good-bye!");
conn.send(pre);
//Tanquem la conexio
try {conn.close();} catch (Exception e) {}
System.out.println("Tancat");
conn.disconnect();
System.out.println("Desconectat");
}
}
Modulo de Criptografia
Descripción
Este modulo ofrece las funciones básicas para cifrar y descifrar los mensajes que se envian y reciben, respectivamente.
Cada cliente tendrá un par de claves, una publica y otra privada. Cuando un cliente quiere enviar un mensaje cifrado, tiene que conseguir la clave publica del destinatario y cifrar con ella el mensaje antes de enviarlo. Al recibirlo, el destinatario usara su clave privada para descifrarlo y recuperar así el mensaje original.
Será necesario pues, que cada cliente pueda dar a conocer su clave publica, y el modo de hacerlo será a través de la 'vCard'. Esta viene a ser como una tarjeta de visita que cada cliente jabber puede almacenar en el servidor donde tiene abierta su cuenta.
La 'vCard' puede contener diferentes campo: el nickname, la dirección de correo electronico, etc. Pero en especial, puede contener un campo 'KEY' donde almacenar la clave publica del cliente. Así pues, cuando un cliente quiera conocer la clave publica de otro, no tiene más que solicitar su 'vCard' y consultar el campo 'KEY' (concretamente el subcampo 'CRED'). Este campo permite además guardar una cadena de texto que indique que tipo de clave se guarda en el (subcampo 'TYPE'). Usaremos esta cadena de texto para distinguir la clave publica usada por nuestro cliente de otras claves que pudieran usar otros clientes.
Hay que tener en cuenta pero, que este cifrado cliente-cliente (puesto que el servidor se limita a entregar el mensaje y es el receptor quien tendrá que descifrarlo) solo funcionará si tanto el emisor como el receptor usan el cliente jabber que se está desarrollando en este proyecto, ya que pese a que el protocolo jabber dispone de un formato de mensaje encriptado, no establece de manera clara un metodo estandar para hacer dicho cifrado. Así pues, si bien es cierto que en la documentación del protocolo se insinua el posible uso de PGP o GPG, no hay ninguna garantia que un cliente que implemente esta funcionalidad use uno de estos dos metodos. Por este motivo, se ha optado por un metodo propio que nos permitirá ademas, aprovechar codigo que ya teniamos escrito y ahorrar el tiempo de tener que leerse la documentación de PGP o GPG.
Como nota final, hay que decir que en realidad tanto la clave publica como la privada no son una unica clave, sino dos. Es decir, realmente hay dos claves publicas y dos claves privadas. El motivo es que los mensajes no solamente se envian cifrados sino también firmados digitalmente. Por ello hacen falta dos claves, una para cifrar (publica) y descifrar (privada), y otra para firmar (privada) y verificar la firma (publica). El emisor usara su clave privada de firma para firmar el mensaje y el receptor usara la clave de firma publica del emisor para verificar que la firma es correcta. La clave de firma pública tendra pues que añadirse también a la 'vCard' para que cualquiera pueda consultarla.
Implementación
Cripto.java
Esta es la clase principal de este modulo.
Empezaremos definiendo su interficie, que al fin y al cabo es lo que los otros modulos van a necesitar conocer para interactuar con este.
class Cripto {
public Cripto(Stream conn, String fitxer, String clau) {...}
public Message xifrarMissatge(Message msg) {...}
public Message desxifrarMissatge(Message msg) {...}
}
Hablemos del constructor:
public Cripto(Stream conn, String fitxer, String clau)
throws StreamException, IOException, VCardNotFoundException
Recibe tres parámetros. La conexión con el servidor, el nombre del fichero que hace de almacen de claves y la clave que permite cifrar/descifrar dicho almacen de claves.
El almacen de claves es simplemente un fichero de texto donde se guardan las claves publicas y las privadas (tanto las de cifrado como las de firma) y que a su vez, esta cifrado usando el algoritmo AES para evitar que nadie pueda leer nuestras claves privadas (las publicas están a disposición de todos en la 'vCard').
Cuando se crea una instancia de esta clase, primero de todo se abre el fichero que hace de almacen de claves y se descifra su contenido usando la clave pasada como parámetro. Luego se obtiene la propia 'vCard' a través de la conexión con el servidor y se comparan las claves publicas que hay en el servidor con las del almacen. Si no concuerdan, se actualiza convenientemente la 'vCard'.
Si el fichero de claves no existe, se generan automaticamente los dos pares de claves publicas y privadas, se crea el almacen (cifrando su contenido con la clave indicada por el usuario) y se actualiza la 'vCard'.
Por lo que se refiere a las excepciones que este metodo puede generar, 'StreamException' y 'IOException' pueden originarse debido a problemas al leer/escribir en la conexión o el fichero, respectivamente; 'VCardNotFoundException' es una excepcion propia, que indica que no se ha podido obtener la 'vCard' del servidor.
Hablemos ahora de como se cifra un mensaje:
public Message xifrarMissatge(Message msg)
throws StreamException, UserNotSupportEncryptionException,
MessageWithoutRecipientException
Este metodo recibe un objeto mensaje de las JSO y devuelve otro objeto, diferente del primero, que es el resultado de haber aplicado el procedimiento de cifrado.
Para ver como es este procedimiento de cifrado, supongamos que tenemos un mensaje tal como el siguiente:
<message to='grf@jabber.org' from='guillemrf@jabberes.org'>
<body>Esto es un mensaje de prueba</body>
</mensaje>
El resultado de aplicarle este metodo tendrá el siguiente aspecto:
<message to='grf@jabber.org' from='guillemrf@jabberes.org'>
<body>This message has been encrypted by PXCJabber</body>
<x xmlns='jabber:x:encrypted'>
mensaje_cifrado
</x>
</mensaje>
Donde 'mensaje_cifrado' es una cadena de texto codificada en base64 y que tiene la siguiente estructura:
mensaje_cifrado = cifrado_rsa(clave_sesion) +
cifrado_aes(mensaje_original+firma, clave_sesion)
Vemos que se utilizan dos algoritmos. El AES, de clave privada y más rapido, para cifrar el mensaje original previamente firmado; y el RSA, de clave publica y mucho más lento, para cifrar la clave de sesion usada en el AES previo.
La cuestion es que el AES al ser un algoritmo de clave privada necesita la misma clave para cifrar y descifrar. En nuestro caso, esta es la clave de sesion, que se genera aleatoriamente y se añade, cifrada con el RSA, al principio del mensaje para que el destinatario pueda luego utilizarla.
De las excepciones que este metodo puede lanzar, destacamos 'UserNotSupportEncryptionException' que indica que el destinatario del mensaje no tiene las correspondientes claves publicas en su 'vCard', y 'MessageWithoutRecipientException' que indica un mensaje sin destinatario, de modo que como no lo conocemos no podemos obtener su clave publica de cifrado.
Finalmente, veamos como se descifra un mensaje:
public Message desxifrarMissatge(Message msg)
throws StreamException, UserNotSupportEncryptionException,
InvalidEncryptedMessageFormatException,
SignatureVerificationFailedException,
MessageWithoutSenderException
Este metodo es muy similar al anterior. Recibe un mensaje, supuestamente cifrado segun el procedimiento que acabamos de ver, y devuelve un nuevo objeto mensaje resultado de hacer el descifrado.
Por lo que al proceso de descifrado se refiere, consiste unicamente en hacer los pasos inversos al de cifrado.
En cuanto a la excepciones, destacar: 'InvalidEncryptedMessageFormatException' que indica que el texto encriptado no tiene el formato esperado, 'SignatureVerificationFailedException' que dice que la firma del mensaje no es valida, y 'MessageWithoutSenderException' que indica que el mensaje no tiene atributo 'from' y que por tanto no podemos obtener la clave de verificacion de firma del emisor del mensaje.
Finalmente, indicar que si este metodo recibe un mensaje que no esta cifrado, es decir que no tiene ..., devuelve el mensaje tal cual (en realidad una copia del mensaje tal cual); y decir también, que si falla la verificación todavia se puede obtener el mensaje descifrado, capturando la excepcion y llamando a su metodo 'getMsg'.
Veamos ahora la clase auxiliar encargada de gestionar la 'vCard'.
VCard.java
Tiene el siguiente aspecto:
class VCard {
public static VCard getVCard(Stream conn)
public static VCard getVCard(Stream conn, JID user)
public static void setVCard(Stream conn, VCard vcard)
public VCard(Extension vcard)
public Clau[] getCredentials()
public void setCredentials(ClauPublica publicKey,
ClauFirma signatureKey)
}
Destacar que 'Clau' es una interficie implementada por tres clases: 'ClauPublica', 'ClauPrivada' y 'ClauFirma'.
Vemos que la clase tiene tres metodos estáticos (pueden ser llamados sin crear ninguna instancia de la clase). 'getVCard' permite obtener del servidor la propia 'vCard' (el primer metodo o el segundo si dejamos user a 'null') o la 'vCard' de algun otro usuario. 'setVCard' recibe un objeto VCard y lo guarda en el servidor.
En cuanto a los otros metodos, el constructor recibe un elemento xml de tipo ... (en general obtenido del servidor), y 'getCredentials' y 'setCredentials' permiten indicar o extraer las dos claves publicas (cifrado y firma).
'getCredentials' devuelve un array de 2 claves, donde el elemento 0 es la clave publica de cifrado y el elemento 1 es la clave de verificacion de firma.
Hay que decir que esta es una clase auxiliar y que por tanto no deberia ser necesario llamarla desde otros modulos sino solamente desde Cripto.java.
Observaciones
Este modulo hace uso de unas clases ya programadas que son: base64.java, sha1.java, aes.java, aritmetica.java y rsa.java, dsa.java y sistemaCriptografic.java
Se han invertido unas 15 horas de programación, más 1 hora de pruebas, más unas 3 horas de documentación.
La parte más complicada ha sido posiblemente la gestión de la 'vCard'.
Pruebas
Basicamente, las pruebas han consistido en comprobar la coherencia de las operaciones de cifrar y descifrar. Generamos un mensaje, lo ciframos, lo desciframos, y comprovamos que el resultado final es igual al mensaje original.
...
Cripto c = new Cripto(conn, "claus", "guillem");
System.out.println("Modul criptografic inicialitzat");
Message msg1 = (Message)sdf.createPacketNode(
sdf.createNSI("message", conn.getDefaultNamespace()),
Message.class);
msg1.setTo(sdf.createJID("grf@jabber.org"));
msg1.setFrom(sdf.createJID("guillemrf@jabberes.org"));
msg1.setBody("Això és un missatge de prova");
System.out.println("Missatge abans:");
System.out.println(msg1.toString());
System.out.println("Xifrant missatge");
Message aux = c.xifrarMissatge(msg1);
System.out.println("Missatge xifrat:");
System.out.println(aux.toString());
System.out.println("Desxifrant missatge");
Message msg2 = c.desxifrarMissatge(aux);
System.out.println("Missatge despres:");
System.out.println(msg2.toString());
System.out.println("Comparant");
if (msg2.toString().equals(msg1.toString())) {
System.out.println("OK");
} else {
System.out.println("Fallada");
}
...
Esta prueba ha revelado que el modulo funciona como era de esperar.
Modulo de Login
Descripción
El módulo de Logín será la ventana encargada de solicitar el usuario y el password al cliente. De este modo podremos hacer la autentificación para conectarse con el servidor de Jabber.
La ventana incorpora los siguientes campos:
- usuario: nombre de usuario que identificará al cliente.
- password: password de acceso para ese usuario.
- host: servidor jabber con el que deseamos comunicarnos.
- ssl: determina si se desea conexión segura.
La idea inicial es crear el formulario utilizando las librerías de java awt y swing.
El primer paso pasará por una correcta documentación. A partir de este punto pasaremos a analizar los diferentes requisitos de la clase.
Será importante realizar unas pruebas modulares para poder observar el comportamiento. De este modo nos libraremos de problemas en el momento de la integración de este módulo dentro del proyecto.
Implementación
Login.java
La clase Login es del tipo JFrame que es la encargada de hacer una ventana en swing.
El Constructor de la clase llama al método Imprimir que será el encargado de generar el formulario.
El método añade 3 parámetros. El usuario, password y el host. El motivo es que puede que necesitemos usar unos datos por defecto. De este modo podremos ponerlos sin problemas.
public void Imprimir(String rusr, String rpsr, String rhost) {
//Creamos las instancias de los objetos del formulario...
txt_usr = new JLabel("Usuario:");
txt_psr = new JLabel("Password:");
txt_host = new JLabel("Host:");
usr = new JTextField();
host = new JTextField();
psr = new JPasswordField(20);
ssl = new JCheckBox("Usar SSL", true);
btn_aceptar = new JButton("Loginar >>");
//Ponemos los valores por defecto pasados por parámetro...
host.setText(rhost);
usr.setText(rusr);
psr.setText(rpsr);
getContentPane().setLayout(null);
//Establecemos las coordenadas de posición absoluta...
txt_usr.setBounds(10,5,65,25);
txt_psr.setBounds(10,35,65,25);
txt_host.setBounds(10,65,65,25);
usr.setBounds(100,5,200,25);
psr.setBounds(100,35,200,25);
host.setBounds(100,65,200,25);
ssl.setBounds(98,95,100,25);
btn_aceptar.setBounds(200,95,100,25);
//Añadimos los componentes en el formulario...
getContentPane().add(usr);
getContentPane().add(ssl);
getContentPane().add(txt_usr);
getContentPane().add(psr);
getContentPane().add(txt_psr);
getContentPane().add(host);
getContentPane().add(txt_host);
getContentPane().add(btn_aceptar);
//Asignamos el evento del boton...
btn_aceptar.addActionListener( this );
}
La parte más importante de toda la clase es aquella de la cual podemos consultar los valores introducidos en la ventana.
Hemos añadido funciones para cada una de las consultas que nos puedan surgir.
//Retorna el usuario...
public String getUsr() {
return usr.getText();
}
//Retorna el password...
public String getPsr() {
return psr.getText();
}
//Retorna el host...
public String getHost() {
return host.getText();
}
//Retorna si se usa SSL...
public boolean getSSL() {
return ssl.isSelected();
}
Observaciones
Una vez estén introducidos los datos, será necesario enviarlos usando el modulo de conexión.
El modulo de conexión accederá a las funciones de consulta permitiendo obtener los datos introducidos por el cliente.
A partir de este punto se generará una conexión que mantendremos durante toda la comunicación.
Al introducir los componentes en la ventana de manera absoluta podemos encontrarnos que en ciertas resoluciones no se vea la ventana correctamente.
Se han invertido un total de 5 horas en este módulo, divididas en:
- 2 horas: documentación librerías gráficas awt y swing.
- 3 horas: implementación de la clase y pruebas.
- 1 hora: generación de la documentación del módulo.
La tarea más complicada ha sido posicionar los diferentes componentes dentro del formulario.
Pruebas
Para realizar las pruebas pertinentes del modulo hemos introducido las siguientes líneas de código.
En primer lugar controlamos los eventos del botón, ahora cuando presionemos el boton podremos ver los datos introducidos.
//Se genera el evento del boton..
public void actionPerformed( ActionEvent evt ) {
System.out.println( "Usr: " + getUsr() );
System.out.println( "Psr: " + getPsr() );
System.out.println( "Host: " + getHost() );
System.out.println( "SSL: " + getSSL() );
}
Seguidamente hemos creado un main de prueba.
//Main de pruebas...
public static void main( String args[] ) {
//Creo la ventana con el título pasado por parámetro...
JFrame ventana = new Login("Login de Usuario");
ventana.addWindowListener( new WindowAdapter() {
public void windowClosing( WindowEvent evt ){
System.exit( 0 );
}
} );
//Tamaño de la ventana...
ventana.setSize( 320,170 );
ventana.setVisible( true );
}
Todo el módulo ha funcionado con completa normalidad.
Modulo de Chat
Descripción
Este modulo consiste en la ventana donde un cliente determinado puede hablar con otro cliente. En concreto será un formulario de Chat donde veremos toda nuestra conversación.
El formulario contendrá los siguientes componentes:
-JTextPane: Listado de texto donde veremos la conversación en curso.
-JTextField: Donde introduciremos el mensaje a enviar.
-JButton: Botón para enviar el mensaje.
-JScrollPane: Scroll vertical para la conversación en curso.
Este formulario será especialmente complejo por la posibilidad de introducir smileys en nuestra conversación.
Los diferentes smileys serán almacenados en una carpeta instalada en el cliente. En concreto en la carpeta de imágenes.
Serán diferentes imágenes en formato *.gif (Pueden introducirse gif´s animados).
Implementación
Chat.java
En primer lugar declararemos los posibles gestos que generan los smileys.
En este caso podemos ver los gestos:
:) abrirá: imagenes/0.gif
:( abrirá: imagenes/1.gif
:S abrirá: imagenes/2.gif
etc...
private String[] iconos = {":)",":(",":S",":O",":R",":P"};
Método que permite generar el formulario a partir del cual podremos conversar con otro cliente.
//Visualiza los objetos del formulario...
public void Imprimir() {
//Iniciamos el listado de conversa...
lista = new JTextPane();
//Establecemos los estilos de texto...
iniciarEstilos(lista);
//Le añadimos el scroll...
lista.setEditable(false);
jp = new JScrollPane(lista);
jp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
//Creamos el botón y mensaje...
mensaje = new JTextField();
btn_enviar = new JButton("Enviar >>");
getContentPane().setLayout(null);
//Establecemos las coordenadas absolutas...
mensaje.setBounds(10,215,270,40);
btn_enviar.setBounds(285,215,95,40);
jp.setBounds(10,10,370,200);
//Añadimos los componentes al formulario...
getContentPane().add(mensaje);
getContentPane().add(btn_enviar);
getContentPane().add(jp);
//Gestionamos los eventos...
btn_enviar.addActionListener( this );
mensaje.addActionListener( this );
}
Ahora realizaremos un método para inicializar los estilos del listado que contiene toda la conversación.
También será necesario cargar las imagenes de los smileys.
//Inicia los estilos del listado...
protected void iniciarEstilos(JTextPane textPane) {
Style def = StyleContext.getDefaultStyleContext().
getStyle(StyleContext.DEFAULT_STYLE);
//Estilo por defecto...
Style regular = textPane.addStyle("regular", def);
StyleConstants.setFontSize(def, 14);
StyleConstants.setFontFamily(def, "SansSerif");
//Estilo recibir de otro cliente...
Style s = textPane.addStyle("recibir", regular);
StyleConstants.setItalic(s, true);
//Estilo enviar a otro cliente...
s = textPane.addStyle("enviar", regular);
StyleConstants.setBold(s, true);
//Cargo las imagenes de los smylers...
for (int i=0; i<iconos.length; i++) {
s = textPane.addStyle(iconos[i], regular);
StyleConstants.setAlignment(s, StyleConstants.ALIGN_LEFT);
StyleConstants.setIcon(s, new ImageIcon("imagenes/"+i+".gif"));
}
}
Ahora declaramos los métodos que serán necesarios para interactuar con el protocolo de envió de mensajes.
Establecemos los métodos de enviar y recibir información de otro cliente.
//Método llamado cuando envío datos al otro cliente...
public void Enviar(String txt) {
//Agrego el texto en la conversación...
PonTextoEnLista(txt + newline, "enviar");
}
//Método llamado cuando recibo datos del otro cliente...
public void Recibir(String txt) {
//Agrego el texto en la conversación...
PonTextoEnLista(txt + newline, "recibir");
}
Seguidamente necesitaremos un método para introducir el texto en la conversación.
Esta será la función encargada de determinar si es un texto normal o es un texto con smileys.
//Pone el texto en la lista...
public void PonTextoEnLista(String txt, String estilo) {
Document doc = lista.getDocument();
try {
//Genero la expresión regular para realizar el split...
String reg = new String();
for (int i=0; i<iconos.length; i++) {
reg = reg + iconos[i];
if (i < (iconos.length -1)) {
reg = reg + "|";
}
}
reg = reg.replace("(","\\(");
reg = reg.replace(")","\\)");
String[] tx = txt.split(reg);
String st;
int lon = 0;
for (int i=0;i<tx.length;i++) {
//Pongo solo el texto...
lista.setCaretPosition(doc.getLength());
doc.insertString(doc.getLength(), tx[i],
lista.getStyle(estilo));
lista.setLogicalStyle(lista.getStyle(estilo));
lon = lon + tx[i].length();
if (lon+2 < txt.length()) {
st = txt.substring(lon, lon+2);
//Pongo la carita...
lista.setCaretPosition(doc.getLength());
doc.insertString(doc.getLength(), " ",
lista.getStyle(st));
lista.setLogicalStyle(lista.getStyle(st));
//Pongo espacio...
lista.setCaretPosition(doc.getLength());
doc.insertString(doc.getLength(), " ",
lista.getStyle(estilo));
lista.setLogicalStyle(lista.getStyle(estilo));
lon = lon + st.length();
}
}
} catch (BadLocationException ble) {
System.err.println("No puedo enviar nada...");
}
}
Observaciones
En caso de que deseemos agregar un nuevo smiley solo tendremos que agregar la imagen en la carpeta de imagenes y añadir el gesto en la declaración de los iconos.
Es necesario asegurarse que la carpeta de imagenes sea accesible para el programa.
Al introducir los componentes en la ventana de manera absoluta podemos encontrarnos que en ciertas resoluciones no se vea la ventana correctamente.
Se han invertido un total de 6 horas en este módulo, divididas en:
- 2 horas: documentación librerías gráficas awt y swing.
- 7 horas: implementación de la clase y pruebas.
- 1 hora: generación de la documentación del módulo.
Lo más complicado con sobrada diferencia ha sido realizar la programación de los smileys. Seguidamente también nos ha traído de cabeza el tema del scroll vertical.
Pruebas
Para realizar las pruebas solo tenemos que introducir el texto ha enviar.
Las pruebas se han concentrado en la visualización correcta de los mensajes y los smileys.
Hemos creado un main de prueba.
//Main para pruebas...
public static void main( String argv[] ) throws Exception {
JFrame ventana = new Chat("Privado conmigo");
ventana.addWindowListener( new WindowAdapter() {
public void windowClosing( WindowEvent evt ){
System.exit( 0 );
}
} );
//Tamaño de la ventana...
ventana.setSize( 400,300 );
ventana.setVisible( true );
}
En una primera versión el algoritmo encargado de visualizar los smileys no funcionaba correctamente. Era un error en la generación de la expresión regular.
Una vez solventado todo el módulo ha funcionado con completa normalidad.
Modulo de NuevoContacto
Descripción
El módulo de NuevoContacto es el formulario que usará el usuario de la aplicación para agregar un nuevo contacto.
Tenemos únicamente el campo donde introducir el JID del usuario, así como un botón para aceptar y otro para cancelar.
Al igual que en el resto de los módulos de interfaz, usaremos la librería Swing de Java para implementar esta clase.
Implementación
NuevoContacto.java
La clase NuevoContacto es una extensión JFrame que es la encargada de hacer una ventana en swing.
El constructor recibe como parámetro un objeto de tipo MainGUI, que es el interfaz principal. Lo usaremos al añadir los actionListener a los botones, ya que el evento de pulsar el botón Aceptar debe ser interceptado por mainGUI, y así lo indicamos al hacer el addActionListener (mg). En cambio la acción de cancelar únicamente cierra la ventana, así que podemos capturarlo en la propia clase NuevoContacto.
Así pues, cuando es llamado el constructor crea una nueva ventana con un JTextField para introducir el JID, su correspondiente JLabel informativo y 2 botones, aceptar y cancelar. A continuación añade los ActionListeners, asociando a Aceptar el MainGUI recibido y a Cancelar una referencia "this" a la propia clase NuevoContacto. Finalmente ajusta el tamaño de la ventana y la hace visible.
public NuevoContacto(MainGUI mg)
{
super("Agregar contacto");
txt_jid = new JLabel("JID:");
jid = new JTextField();
btn_aceptar = new JButton("Aceptar >>");
btn_cancelar = new JButton("Cancelar");
getContentPane().setLayout(null);
txt_jid.setBounds(10,10,35,25);
jid.setBounds(40,10,200,25);
btn_aceptar.setBounds(40,45,90,25);
btn_cancelar.setBounds(140,45,90,25);
getContentPane().add(jid);
getContentPane().add(txt_jid);
getContentPane().add(btn_aceptar);
getContentPane().add(btn_cancelar);
btn_aceptar.addActionListener( mg ); //Aceptar es capturado por MainGUI
btn_cancelar.addActionListener(this);//Cancelar cierra la ventana
setSize( 260,120 );
setVisible( true );
}
A continuación, dado que hemos indicado que NuevoContacto captura el evento del botón Cancelar, estamos obligados a definir la operación actionPerformed que será llamada cuando se produzca el evento, y que únicamente oculta la ventana (si no se vuelve a usar, Java se encargará de destruir el objeto).
public void actionPerformed (ActionEvent e)
{
hide();
}
Finalmente, definimos una operación consultora para obtener desde el interfaz principal el JID que ha introducido el usuario.
public String getJid()
{
return jid.getText();
}
Observaciones
Al introducir los componentes en la ventana de manera absoluta podemos encontrarnos que en ciertas resoluciones no se vea la ventana correctamente.
Se han invertido un total de 5 horas en este módulo, divididas en:
- 2 horas: documentación librerías gráficas awt y swing.
- 2 horas: implementación de la clase y pruebas.
- 1 hora: generación de la documentación del módulo.
La tarea más complicada ha sido posicionar los diferentes componentes dentro del formulario.
Pruebas
Para realizar las pruebas pertinentes del modulo hemos introducido las siguientes líneas de código.
Primero hemos definido, dentro de MainGUI, una opción de menú que crea un objeto NuevaConexion, de esta forma:
NuevaConexion nc;
.
.
.
if (e.getActionCommand().equals("Agregar un contacto"))
{
nc = new NuevoContacto (this);
}
A continuación hemos definido la operación actionPerformed, donde imprimimos por pantalla el texto que se ha introducido en el formulario.
if (e.getActionCommand().equals("Aceptar >>"))
{
nc.hide();
System.out.println("JID introducido: "+nc.getJid());
}
Todo el módulo ha funcionado con completa normalidad.
Modulo de ThreadConexion
Descripción
La clase ThreadConexion nace a raíz de un problema que surgió en la integración de la aplicación. Concretamente, una vez desarrollados los módulos conexión, mensajes e interfaz principal (y cada uno probado separadamente) se procede a integrarlos en la aplicación.
El módulo de mensajes utiliza una cola de mensajes proporcionada por las liberías JSO para efectuar el envío de mensajes.
Conexión utiliza un sistema similar para gestionar la recepción de los mismos. Para que este sistema funcione, debe habe un bucle que constantemente compruebe si hay algun mensaje en la cola y lo procese. El problema surge cuando se inicia este bucle, ya que bloquea la aplicación, impidiendo al usuario interactuar con el interfaz gráfico.
La única solución viable para evitar este bloqueo es implementar una de las dos partes implicadas (interfaz o bucle de envío/recepción de mensajes) en un thread. Nos decidimos por el segundo, ya que es una parte de código mucho más reducida y más independiente del resto de módulos que el interfaz.
Implementación
ThreadConexion.java
La clase ThreadConexion tiene únicamente 3 métodos:
public ThreadConexion (conexion con)
{
try
{
c=con;
conn = c.obtConex();
}
catch(Exception e)
{
System.out.println("(ThreadConexion)Error: "+e.getMessage());
}
}
Constructor de la clase. Recibe una conexion como parámetro, obtiene el Stream de esa conexión y guarda ambos como atributos locales.
public void run()
{
while (c.isContinue())
{
Packet data = null;
try
{
conn.process();
try { Thread.sleep(10); } catch (InterruptedException ie) {}
while ((data = c.deque()) != null)
conn.send(data);
}
catch(Exception e)
{
System.out.println("(ThreadConexion)Warning: "+e.getMessage());
}
}
}
Es el método que se ejecuta cuando se inicia el thread. Contiene un bucle que se mantiene mientras la conexión existe, y que constantemente lee el contenido de la cola de mensajes de la conexión (c.deque()). En caso de encontrar algun mensaje, lo envia a través del Stream creado en el constructor.
public void desconectar()
{
c.desconectar();
}
Método usado para desconectar. Al ser ejecutado, la conexión finaliza, y en consecuencia también acaba el bucle del método run(). Lo usamos para permitir la finalización del thread de conexión al desconectar o finalizar la aplicación.
Observaciones
El planteamiento inicial de esta clase era que contuviera gran parte del código dedicado a gestionar la conexión. El problema surgido de este planteamiento es la difícil comunicación entre un thread y los módulos asociados a la clase que lo ha ejecutado.
A raíz de estos problemas optamos por reubicar toda la parte de código que se comunicaba con otras clases (especialmente con interactua, que es la clase que hace de puente con las librerías JSO y el paso de mensajes) en el interfaz principal, quedando ThreadConexion reducida únicamente al bucle de envío/recepción.
Se han dedicado aproximadamente 5 horas en la realización de este módulo:
- Documentación: 2 horas
- Desarrollo y pruebas: 3 horas
Pruebas
Para probar este módulo hubo que esperar a la primera versión de integración de la aplicación con el interfaz principal y los módulos de conexión, mensajes e interactua. Se hicieron en 2 fases:
Una primera fase para probar que realmente funcionaba, fue colocar una operación de impresión por pantalla dentro del bucle del thread que informara de todo aquello que iba recibiendo.
A continuación insertamos la creación del thread dentro del interfaz principal, ejecutamos la aplicación y, mediante otro cliente Jabber (como Exodus o Gaim, por ejemplo) mandamos mensajes a nuestro cliente. Observamos que todo lo que llegaba era imprimido por pantalla, y confirmamos que este sistema era correcto.
La segunda fase de pruebas se realizó cuando el módulo de Chat fue integrado con el resto del sistema. Se pudo observar que al iniciar una conversación todos los mensajes eran enviados y recibidos satisfactoriamente.
Modulo de Interface Principal
Descripción
Este módulo consiste en la interface principal de la aplicación. Contiene el menú con todas las operaciones que puede realizar el usuario y la lista de los contactos con los que podrá iniciar una conversación.
Al ser la interface principal, ésta deberá estar conectada con todos los módulos auxiliares y comunicarse con ellos. Asimismo deberá mantener las estructuras de datos que deben ser persistentes a lo largo de toda la ejecución y mandarlas a los módulos correspondientes.
El formulario contendrá los siguientes componentes:
-JTree: Arbol donde se mostrará la lista de contactos.
-JMenu: Menu de opciones.
-JScrollPane: Scroll vertical para la lista de contactos.
Las operaciones que permite son:
- Conectar
Inicia el módulo Login (comentado posteriormente)
Obtiene los datos del módulo Login
Comprueba que los datos sean correctos
Inicia la conexión con el servidor Jabber y los datos recibidos
Manda el estado de presencia inicial
Recupera la lista de contactos y actualiza el árbol de contactos
Inicia el thread de conexión que gestionará los mensajes
- Desconectar
Manda el estado de presencia de desconexión
Desconecta del servidor
Finaliza el thread de conexión
- Cerrar
Hace las mismas acciones que desconectar y finaliza la aplicación
- Añadir contacto
Inicia el módulo NuevoContacto (comentado posteriormente)
Obtiene los datos del módulo NuevoContacto
Comprueba que los datos sean correctos
Manda una petición de subscripción a ese contacto
- Cambio de estado
Manda un cambio de presencia con la selección hecha
Implementación
MainGUI.java
Definimos las estructuras de datos necesarias:
//Datos de conexion
String jid;
String passwd;
String host;
int port;
//Lista de contactos
ArbolContactos lc;
//Datos de gestion JSO
conexion _conexion;
Stream conn=null;
StreamDataFactory sdf = null;
interactua _interactua = null;
ThreadConexion buclePrincipal=null;
//Formularios auxiliares
NuevoContacto nc; //Agregar contacto
Login ventana; //Conectar
A continuación describimos paso a paso el constructor de la interface.
Inicialmente llamamos al constructor de JFrame, que es la clase de la que heredamos, para que cree la ventana vacía con el título
public MainGUI()
{
super("PXC Jabber Client!");
Luego definimos un MouseListener, un objeto que atiende los eventos producidos por el ratón. Este objeto se lo pasaremos al árbol de contactos, y en su interior definimos una operación que será llamada al hacer doble click. De esta forma podemos capturar desde la interface principal (que es donde tenemos la información de la conexión y los datos necesarios) el doble click producido sobre un contacto (que es cuando actuará).
MouseListener ml = new MouseAdapter(){
public void mousePressed(MouseEvent e) {
if(e.getClickCount() == 2)
{
if (_interactua.contactStatus(lc.getContactoSeleccionado())!=null)
{
String tmp = new String(_interactua.contactStatus(lc.getContactoSeleccionado()));
if ((tmp.compareTo("offline")!=0) && (tmp.compareTo("unsubscribed")!=0))
{
Chat a = new Chat(lc.getContactoSeleccionado(), _conexion, _interactua);
a.show();
}
}
}
}
};
A continuación definimos el interfaz gráfico, que consiste en un JMenu y un objeto ArbolContactos (comentado a continuación):
lc = new ArbolContactos(ml);
JPanel cp = new JPanel();
cp.setLayout(new BorderLayout());
.
.
.
(creación del menú)
.
.
cp.add(lc, java.awt.BorderLayout.CENTER);
setContentPane(cp);
setJMenuBar (jmb);
setSize(300,500);
addWindowListener( new WindowAdapter() {
public void windowClosed( WindowEvent evt ){
System.exit(0);}});
}
Posteriormente definimos las acciones que responden a los eventos producidos en la aplicación.
Estos pueden ser:
- Opción del menú seleccionada
- Botón pulsado en un formulario auxiliar
Y tienen la siguiente estructura:
if (e.getActionCommand().equals("Opcion a capturar"))
{
... (código a ejecutar) ...
}
Primero veamos las opciones del menú en detalle:
- Conectar: Muestra la ventana de login
ventana = new Login("Login de Usuario", this);
ventana.show();
- Desconectar: En caso de que estemos conectados, desconecta del servidor y limpia la lista de contactos
if (buclePrincipal!=null)
{
_conexion.desconectar();
buclePrincipal.desconectar();
//Esperamos a que finalice el thread
try { Thread.sleep(10); } catch (InterruptedException ie) {}
buclePrincipal=null;
lc.clear();
}
- Cerrar: Desconecta y finaliza la aplicación. Nótese que en este caso no hace falta finalizar el thread ya que la propia VM de Java lo hace al finalizar la ejecución.
// Desconectamos
if (buclePrincipal!=null)
_conexion.desconectar();
System.exit(0);
- Agregar un contacto: Muestra la ventana de nuevo contacto
nc = new NuevoContacto (this);
- Cambio de estado: Creamos un objeto de presencia y le asignamos el estado que corresponda según la opcíón del menú escogida (available, away, dnd (do not disturb) o offline) y lo enviamos.
Presence pre = (Presence)sdf.createPacketNode(sdf.createNSI("presence", conn.getDefaultNamespace()));
pre.setStatus("available");
pre.setShow("available");
try
{
conn.send(pre);
}
catch(StreamException ex)
{
JabberException.mensajeError("Error al enviar cambio de estado: "+ex.getMessage());
}
A continuación se comentan las funciones que se ejecutan cuando se capturan eventos que provienen de los formularios auxiliares. Concretamente capturamos 2 eventos:
- Login (se pulsa Loginar desde el formulario de Login)
Cuando se produce este evento ocultamos el formulario de login, obtenemos los datos del formulario y ejecutamos la operación de conectar.
ventana.hide();
getData();
connect();
- Aceptar (se pulsa Aceptar desde el formulario de Nuevo Contacto)
Igual que en caso anterior, ocultamos el formulario y ejecutamos la operacion nuevoContacto de interactua con los datos obtenidos del formulario
nc.hide();
_interactua.nuevoContacto(nc.getJid());
Llegados a este punto sólo falta comentar las 2 funciones que estan definidas en esta clase y que se usan para iniciar la conexión al servidor. Una vez conectados, usaremos las estructuras de datos iniciadas después de la conexión para delegar el resto de operaciones al resto de módulos de la aplicación.
Las funciones son:
- getData()
Obtiene los datos del formulario de Login
void getData()
{
jid = new String(ventana.getUsr());
passwd = new String(ventana.getPsr());
host = new String(ventana.getHost());
port = 5222;
}
- connect()
Inicia la conexión al servidor. Inicialmente crea un nuevo objeto conexion, pasándole como parámetros los datos obtenidos anteriormente, y ejecuta la operación login().
En este punto ya estamos conectados al servidor, y podemos iniciar las estructuras de datos Stream y StreamDataFactory, que se necesitarán posteriormente, ya sea para enviar mensajes o para pasárselos a otros módulos auxiliares.
Acto seguido recibimos la lista de contactos, y a continuacion creamos un nuevo estado de presencia y lo enviamos al servidor. De esta forma se notificará a todos nuestros contactos que estamos conectados y en estado "disponible".
Posteriormente se crea una instancia de interactua. Este objeto hará de intermediario entre el protocolo Jabber y el interface gráfico.
Llegados a este punto creamos un thread concurrente de la clase ThreadConexion, que se comentará posteriormente. A grandes rasgos, el thread se encarga de estar siempre a la escucha de paquetes que llegan o salen de la aplicación sin bloquearla.
Finalmente, se actualiza el árbol de contactos con la lista recibida anteriormente, y se notifica al objeto ineractua con esta lista de contactos, para que sepa quienes pueden ser el origen de mensajes.
private void connect()
{
try
{
//Creamos la conexion con el servidor
_conexion = new conexion (jid.concat("/PXCCLient"),passwd,host,5222);
_conexion.login();
conn = _conexion.obtConex();
sdf = conn.getDataFactory();
contactos MisAmigosC=new contactos(_conexion);
/* DEFINIMOS UN ESTADO DE PRESENCIA INICIAL */
Presence pre = (Presence)sdf.createPacketNode(sdf.createNSI("presence", conn.getDefaultNamespace()));
pre.setStatus("available");
conn.send(pre);
_interactua = new interactua(_conexion, MisAmigosC);
//Lanzamos el thread de conexion
buclePrincipal = new ThreadConexion(_conexion);
buclePrincipal.start();
lc.actualizar(MisAmigosC);
_interactua.addArbolContactos(lc);
}
catch (UnknownHostException e)
{
JabberException.mensajeError("Host desconocido");
}
catch(Exception e)
{
JabberException.mensajeError("Error al conectar con el servidor: "+e.getMessage());
}
}
ArbolContactos.java
Esta clase se encarga de gestionar el árbol de contactos a nivel visual. Podríamos decir que de alguna forma conecta la lista de contactos definida en nuestra aplicación con la lista de objetos del JTree insertado en la ventana.
Veamos primero el constructor.
public ArbolContactos(MouseListener ml)
{
super(new GridLayout(1,0));
rootNode = new DefaultMutableTreeNode("Contactos");
treeModel = new DefaultTreeModel(rootNode);
tree = new JTree(treeModel);
tree.setEditable(false);
tree.getSelectionModel().setSelectionMode
(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setShowsRootHandles(true);
cg = new ContactoGrafico();
tree.setCellRenderer( cg );
JScrollPane scrollPane = new JScrollPane(tree);
add(scrollPane);
tree.addMouseListener(ml);
}
El constructor recibe como parámetro el MouseListener que hemos comentado en el apartado anterior. Define un treeModel (estructura lógica del árbol gráfico) vacío, con el nodo raíz llamado "Contactos". A continuación define el JTree propiamente dicho, así como sus propiedades.
Es importante hacer hincapié en la operación setCellRenderer. El problema que nos encontramos es que no queremos que el árbol de contactos tenga el aspecto estándar, ya que queremos que junto al nombre de los contactos aparezca un icono que identifique su estado. Para solucionarlo, creamos un objeto ContactoGrafico (comentado posteriormente), y lo utilizamos en la llamada a la operación setCellRenderer. Esta operación sustituye la forma de mostrar visualmente los nodos por la definida en ContactoGrafico.
Finalmente se añaden scroll y el MouseListener.
Pasamos ahora a comentar los servicios que ofrece esta clase y que utilizamos desde MainGUI:
public String getContactoSeleccionado()
Retorna un String conteniendo el nombre del contacto seleccionado
public void clear()
Elimina todos los nodos del árbol excepto la raíz
public void actualizar(contactos _contactos)
Actualiza el árbol de contactos a partir de la información del objeto contactos que recibe como parámetro.
Esta operación será llamada cada vez que se detecte algun cambio en la lista de contactos, como el cambio de estado de alguien o la adición de un contacto nuevo.
Cuando se llama a esta operación se vacía el árbol de contactos (mediante la operación clear()), y se vuelven a añadir tantos elementos como contactos tenemos, teniendo en cuenta el estado de cada uno.
Finalmente se llama a la operación actualizar() del ContactoGrafico para volver a definir el aspecto de cada nodo del árbol.
ContactoGrafico.java
La clase ContactoGrafico define el aspecto de los nodos del árbol. Una vez creada la instancia de la clase, se mantiene una lista de contactos y de estados, asi como una lista de los iconos disponibles.
La principal función de la clase es getTreeCellRendererComponent. Esta función, definida por el interface CellTreeRenderer de Java, es llamada automáticamente por el JTree cada vez que se refresca la imagen del árbol de contactos. Toda la gestión del refresco es gestionada internamente por la VM de Java, lo cual facilita mucho el trabajo. Lo único que tenemos que hacer es definir en esta función el aspecto de cada nodo según el texto que contiene (que, en última instancia, es el JID del contacto).
public Component getTreeCellRendererComponent( JTree arbol,
Object valor,boolean seleccionado,boolean expandido,
boolean rama,int fila,boolean conFoco )
{
// Hay que encontrar el nodo en que estamos y coger el
// texto que contiene
DefaultMutableTreeNode nodo = (DefaultMutableTreeNode)valor;
String texto = new String((String)nodo.getUserObject());
this.seleccionado = seleccionado;
if( !seleccionado )
setForeground( Color.black );
else
setForeground( Color.white);
if (nodo != null)
{
if (texto.equals("Contactos"))
setIcon(icono[0]);
else
setIcon(icono[getIconByStatus(texto)]);
setText(texto);
}
return(this);
}
Esta función es llamada automáticamente para cada nodo del árbol. Define 3 componentes gráficas: el color del texto, el icono y el texto. El color se decide según el contacto esté seleccionado o no (esto es un parámetro de la operación). El texto se obtiene del objeto nodo, recibido tambien como parámetro. Finalmente el icono se obtiene consultando el estado del contacto.
Llegados a este punto sólo falta redefinir el color de fondo. Este parámetro no se puede modificar desde getTreeCellRendererComponent. Para cambiarlo deberemos redefinir la función Paint(), que en última instancia es la función que se llama para dibujar cualquier objeto gráfico.
public void paint( Graphics g )
{
Color color;
Icon currentI = getIcon();
// Fijamos el colos de fondo
color = seleccionado ? Color.red : Color.white;
g.setColor( color );
// Rellenamos el rectángulo que ocupa el texto sobre la
// celda del árbol
g.fillRect( 0,0,getWidth()-1,getHeight()-1 );
super.paint( g );
}
Observaciones
Para la realización de este módulo se han invertido aproximadamente 14 horas:
- 2 horas: documentación librerías gráficas awt y swing.
- 10 horas: implementación de la clase y pruebas.
- 2 horas: generación de la documentación del módulo.
Se podría decir que la implementación de este módulo se ha realizado en 2 fases, cada una de ellas con sus problemas derivados.
Una primera fase ha sido la implementación básica de las 3 clases y conseguir que los elementos del árbol tuvieran el aspecto deseado. Esta parte incluye el diseño de los iconos y las primeras pruebas.
La parte más compleja ha sido idear un sistema para que el getTreeCellRendererComponent sea capaz de decidir el icono que le corresponde a un contacto, ya que esta función está definida por el interface CellTreeRenderer y en consecuenca no se le pueden pasar atributos propios.
La solución ha sido mantener la lista de contactos y estados en la propia clase, creada en el contructor y mantenida por la operación actualizar.
La segunda fase consiste en la integración de estas clases con el conjunto de la aplicación. Algunos aspectos han sido especialmente problemáticos, como por ejemplo la conexión entre el evento doble-click (generado en el JTree), el contacto con quien se inicia la conversación y la ventana de Chat. La solución, como se ha comentado anteriormente, ha sido definir un objeto MouseListener donde se implementa la función de respuesta y pasárselo al constructor de ArbolContactos.
Pruebas
Las pruebas de estas clases se han realizado conjuntamente, ya que dependen unas de las otras.
En realidad, las pruebas iban dirigidas especialmente a las clases que gestionan el árbol de contactos (ArbolContactos y ContactoGrafico), ya que probar el interfaz principal MainGUI es trivial (simplemente ejecutar y ver si su aspecto es satisfactorio).
Posteriormente se realizarán las pruebas de integración, donde se comprobará la conexión del interfaz principal con el resto de módulos.
Para realizar las pruebas se ha implementado una versión temporal de la función actualizar de la clase ArbolContactos, donde en vez de pasarle un objeto contactos se le pasaban dos listas de Strings, una con los contactos y otra con los estados.
Se ha creado un main de pruebas llamando a actualizar con diferentes parámetros, observando que el comportamiento era satisfactorio.
Modulo de Interactua
Descripción
Este módulo desempeña una función esencial para el funcionamiento del cliente, una vez establecida la conexión hay que inicializar esta clase para que el cliente pueda interceptar los diferentes eventos que le afectan.
Tipos de eventos más importantes:
- Eventos sobre los mensajes
- Eventos sobre el estado
- Eventos sobre las suscripciones
Cada tipo de eventos es tratado de forma diferente.
Es preciso decir que para cada instancia de interactua necesitaremos la conexion y la lista de contactos, así como la conexión no necesita de justificación para incluirla dentro de esta clase, hay que decir que incluimos tambien contactos porque desde interactua se modifican los estados de los contactos a una conexión dada, de manera que es necesario que esten disponibles desde esta clase.
Implementación
interactua.java
Atributos de la clase:
Iniciales:
public conexion _conexion;
public contactos _contactos;
Añadidos a partir de la necesidad del entorno gráfico:
Vector conversaciones = null;
ArbolContactos ac = null;
Funciones de la clase:
Iniciales:
class interactua {
private void IniEscucha() {...}
private String limpia_resource (String entrada) {...}
Añadidas a partir de la necesidad del entorno gráfico:
private String limpia_node (String entrada) {...}
public void addConversacion(Chat nc) {...}
public void addArbolContactos(ArbolContactos a){...}
private Chat encuentraConversacion(String jid){...}
public String contactStatus(String jid){...}
public void nuevoContacto(String jid){...}
private interactua getInteractua(){...}
Constructor de la clase:
public interactua (conexion _conexion1, contactos _contactos1) {...}
El constructor recibe dos parámetros tal y como comentamos en la descripción. Lo que hace el constructor es copiar los datos recibidos y inicializar la escucha a la conexión.
_conexion=_conexion1;
_contactos=_contactos1;
IniEscucha();
IniEscucha es la función principal de éste módulo. Desribimos a continuación el funcionamiento general. Esta función inicializa una escucha para cada tipo de evento que se realiza, en este caso discriminamos los comentados en la descripción del módulo.
Eventos de mensajes:
Cuando recibimos un mensaje tenemos que capturarlo y enviarlo, para dicho caso tenemos una clase especifica mensajes que se encarga de distribuir el mensaje.
try {
watcher = new Myxpl(sdf, "app:message[app:body]", _conexion) {
public void packetMatched(PacketEvent evt)
{
Message in = (Message)evt.getData();
String temp;
mensajes _mensaje = new mensajes();
mensajes _mensaje = new mensajes(_conexion, "pxc_claves", "pxc");
temp = _mensaje.recibir_mensaje(_conexion,in);
System.out.println("He recibido: " + temp);
Chat tmp = encuentraConversacion(from.getNode());
if (tmp!=null)
tmp.Recibir(temp);
else
{
System.out.println("Chat iniciado por: "+limpia_resource(from.toString()));
Chat a = new Chat(limpia_resource(from.toString()), _conexion, getInteractua());
addConversacion(a);
a.show();
a.Recibir(temp);
}
}
addConversación(a) añade una conversación al vector inicial de chats que tenemos. De esta manera se puede llevar un control de las diferentes charlas que tenemos con distintos usuarios.
if (conversaciones == null)
conversaciones = new Vector();
conversaciones.add(nc);
encuentraConversacion(from.getNode()) encuntentra (si es que exite) la conversación, es decir, si ya tenemos una conversación activa con el contacto la recupera.
...
if (conversaciones == null)
return null;
String tmp = new String();
for (int i=0; i<conversaciones.size(); i++)
{
tmp = limpia_node(((Chat)(conversaciones.get(i))).getChateante());
if ((limpia_node(((Chat)(conversaciones.get(i))).getChateante()).compareTo(jid))==0)
{
return (Chat)conversaciones.get(i);
}
}
return null;
...
Eventos sobre presencia:
Tenemos que estar informados en todo momento de la presencia de nuestros contactos (evitar enviar mensajes a usuarios "offline"...).
En este Evento se controlan tres tipos de eventos posibles:
- SUSCRIBE
- UNSUSCRIBE
- OTROS
- SUSCRIBE/UNSUSCRIBE:
Este evento se utiliza para aceptar o no a otros contactos que quieren agregarse a nuestra lista de contactos, el sistema es el siguiente:
Una vez recibimos el evento de SUSCRIBE, preguntamos al usuario si quiere autorizar a el contacto que nos quiere agregar para que estemos en su lista.
En cambio, cuando alguien quiere eliminarnos de su lista, el procedimiento es automático, ya que no es opción decir que no queremos que nos quiten de una lista ajena.
Veamos la parte de codigo que hace posible estos eventos:
Primero vemos como se inicializa el evento.
try {
watcher = new Myxpl(sdf, "app:presence",_conexion) {
public void packetMatched(PacketEvent evt) {
Presence in = (Presence)evt.getData();
Presence out;
Packet.Type type = in.getType();
Definimos una variable Type para saber que tipo es de los anteriores el mensaje recibido.
SUSCRIBE
if (type == Presence.SUBSCRIBE) //alguien quiere darnos de alta
{if (JabberException.mensajeSiNo(in.getFrom().toString()+" quiere agregarle a su lista de contactos. ¿Acepta?")==0)
{
out = (Presence)in.copy();
out.reset();
out.setTo(in.getFrom());
out.setType(Presence.SUBSCRIBED);
_conexion.enque(out);
out = (Presence)in.copy();
out.reset();
out.setTo(in.getFrom());
out.setType(Presence.SUBSCRIBE);
_conexion.enque(out);
_contactos.limpiarContactos();
try{
_contactos.obtenerContactos(_conexion);
}
catch(PacketException e)
{JabberException.mensajeError("Error al actualizar la lista de contactos: "+e.getMessage());
} catch(StreamException e)
{JabberException.mensajeError("Error al actualizar la lista de contactos: "+e.getMessage());
} ac.actualizar(_contactos);
}
UNSUSCRIBE
...
else if (type == Presence.UNSUBSCRIBE) {
out = (Presence)in.copy();
out.reset();
out.setTo(in.getFrom());
out.setType(Presence.UNSUBSCRIBED);
_conexion.enque(out);
out = (Presence)in.copy();
out.reset();
out.setTo(in.getFrom());
out.setType(Presence.UNSUBSCRIBE);
_conexion.enque(out);
}
...
OTROS
...
else
{ JID remitente;
String estado;
remitente = in.getFrom();
estado=in.getStatus();
_contactos.mod_contacto(limpia_resource(remitente.toString()),estado);
...
La función limpia_resource es una función auxiliar que nos convierte los JID del tipo "nombre@direccion/Resource" a
"nombre@direccion"
private String limpia_resource (String entrada)
{ String salida[]=entrada.split("/");
return salida[0];
}
La función limpia_node es una función auxiliar que nos convierte los JID del tipo "nombre@..." a
"nombre"
private String limpia_node (String entrada)
{
String salida[]=entrada.split("@");
return salida[0];
}
La función nuevoContacto sirve para agregar un nuevo contacto a nuestra lista de usuarios, el procedimiento es crear un nuevo elemento de presencia del tipo "SUSCRIBE" y enviarlo al JID que queremos agregar a nuestra lista.
Creación del nuevo elemento de presencia y obtención de los datos necesarios.
...
Stream conn = _conexion.obtConex();
StreamDataFactory sdf = conn.getDataFactory();
JID to;
Presence pre = (Presence)sdf.createPacketNode(sdf.createNSI("presence", conn.getDefaultNamespace()));
...
Envio del objeto presencia tipo "SUSCRIBE".
...
try
{
to = new JID(jid);
pre.setTo(to);
pre.setType(Presence.SUBSCRIBE);
_conexion.enque(pre);
}
catch (JIDFormatException e)
{
JabberException.mensajeError("Error en el formato del JID del contacto");
}
...
Observaciones
Este módulo es el que discrimina los tipos de eventos que vamos recibiendo desde que empieza la ejecución de la conexión hasta que finaliza.
Una de las cuestiones que hay que tener el cuenta es la diversidad de estados de los que disponen los diferentes clientes que hay en el mercado, además de no haber clarificado exactamente un estándard en cuanto a estados se refiere, es decir, cuando un contacto se pone en espera, dependerá en primera instáncia del cliente que utilize que será el que definirá cuál es el calificativo que utiliza para designar dicho estado.
De esta manera nosotros definimos unos estados considerados estándares dentro de nuestro cliente para poder trabajar.
Pruebas
Las pruebas realizadas consisten en poner indicadores dentro de los eventos para comprobar cuando entramos en uno de ellos y comprobar si la información que reciben es correcta. En este caso se hacen pruebas sobre mensajes y sobre presencia.
Mensajes
Añadimos una linea que saca por pantalla el mensaje "in" recibido.
...
try {
watcher = new Myxpl(sdf, "app:message[app:body]", _conexion) {
public void packetMatched(PacketEvent evt)
{
Message in = (Message)evt.getData();
String temp;
mensajes _mensaje = new mensajes();
temp= _mensaje.recibir_mensaje(_conexion,in);
System.out.println(in);
conn.addPacketListener(PacketEvent.RECEIVED, watcher);
} catch (SAXPathException spe) {
}
...
Una vez ejecutado el programa le enviamos las siguientes cadenas "HOLA", "HOLA_2", ":-;'ç{}[]`+?¿'¡" y comprobamos la salida
Salida de la consola:
<message from='nateng@jabber.org/Gaim' to='ss@jabber.org' type='chat' xml:lang='en-US'><x xmlns='jabber:x:event'><composing/></x><body>HOLA</body><html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>hola</body></html></message>
<message from='nateng@jabber.org/Gaim' to='ss@jabber.org/PXCCLient' type='chat' xml:lang='en-US'><x xmlns='jabber:x:event'><composing/></x><body>HOLA_2</body><html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>HOLA_2</body></html></message>
<message from='nateng@jabber.org/Gaim' to='ss@jabber.org/PXCCLient' type='chat' xml:lang='en-US'><x xmlns='jabber:x:event'><composing/></x><body>:-;'ç{}[]`+?¿'¡</body><html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>:-;'ç{}[]`+?¿'¡</body></html></message>
Pruebas sobre presencia:
En las pruebas realizadas sobre la presencia las hacemos siguiendo el mismo procedimiento.
Al conectar nos devuelve:
<presence from='nateng@jabber.org/Gaim' to='ss@jabber.org' xml:lang='en-US'><show>away</show><status>Sorry, I ran out for a bit!</status><x xmlns='jabber:x:delay' from='nateng@jabber.org/Gaim' stamp='20041207T18:21:25'/></presence>
Esto indica que de los contactos que tenemos el unico activo es este.
Cambiamos de estado a "away" de nateng@jabber.org
<presence from='nateng@jabber.org/Gaim' to='ss@jabber.org' xml:lang='en-US'><show>away</show></presence>
Nos desconectamos del cliente nateng@jabber.org
<presence from='nateng@jabber.org/Gaim' to='ss@jabber.org' type='unavailable' xml:lang='en-US'><status>Logged out</status></presence>
Nos volvemos a conectar
<presence from='nateng@jabber.org/Gaim' to='ss@jabber.org' xml:lang='en-US'/>
<presence from='nateng@jabber.org/Gaim' to='ss@jabber.org' xml:lang='en-US'><x xmlns='jabber:x:delay' from='nateng@jabber.org/Gaim' stamp='20041207T18:23:57'/></presence>
Pruebas de añadir contactos:
Las pruebas sobre el añadir un contacto en nuestro cliente se han hecho desde entorno gráfico y funciona correctamente.
Modulo de Conexión
Descripción
Este módulo es el primero que se ejecuta para conseguir que el cliente interactúe con el servidor proporciona una conexión y todos los datos necesarios para gestionar las funciones básicas del cliente.
Implementación
conexion.java
Atributos de la clase:
private Stream _Conn;
private StartTLSSocketStreamSource _TLS;
private JID _JID;
private String _Password;
private boolean _Loggedin;
private IdentityGenerator _IdGen = new IdentityGenerator();
private boolean _Continuar;
private List _PacketQueue = new java.util.LinkedList();
private long _INICIO;
public boolean desconectarse=false;
Funciones de la clase:
class conexion {
public Stream obtConex() {...}
public JID getJID() {...}
public String obtPassword() {...}
public boolean isLoggedIn() {...}
public void setLoggedIn(boolean log) {...}
public long getStartTime() {...}
public void setStartTime(long start) {...}
public boolean isContinue() {...}
public Packet deque() {...}
public void enque(Packet data) {...}
protected void login() throws PacketException, StreamException {...}
protected void autentificacion() throws PacketException, StreamException {...}
public void desconectar () {...}
private StartTLSSocketStreamSource getStartTLSSocketStreamSource(JID servidor,int port){...}
}
Constructor de la clase:
public conexion(String clienteSpec, String pass, String hostSpec, int port) throws IllegalArgumentException, NoSuchAlgorithmException, IOException, UnknownHostException, StreamException {..}
El constructor recibe cuatro parámetros, a partir de aqui se siguen los siguientes pasos para obtener una conexión.
Validaciones previas antes de intentar la conexión.
/* Validación del correcyo formato del primer parámetro */
cliente = sdf.createJID(clienteSpec);
if (!Utilities.isValidString(cliente.getN