6. Persistencia y Manejo del Estado Inicial
En varios de los casos de estudio de este libro, hemos utilizado archivos de datos para configurar el estado inicial de la aplicación. Por ejemplo, en el caso de estudio del empleado (nivel 1) teníamos en un archivo su fotografía. En el caso de estudio de la tienda (nivel 2) teníamos en un archivo la imagen de cada producto. En este nivel, el visor de imágenes utiliza un archivo para leer la imagen que será manipulada por la aplicación. Todos esos ejemplos tienen en común que la información del archivo se emplea para inicializar el estado de la aplicación. En ningún caso hemos guardado resultados del programa en un archivo para hacerlos persistentes cuando la aplicación termine. Este problema de hacer persistir los cambios que hagamos en el estado del mundo está fuera del alcance de este libro.
En esta sección estudiaremos una forma sencilla de leer datos de un archivo, con el propósito de configurar el estado inicial de los elementos del modelo del mundo. Vamos a estudiar los conceptos básicos y luego resolveremos el requerimiento funcional de cargar la información del campeonato desde un archivo.
6.1. El Concepto de Archivo
El concepto de archivo no es nuevo para nosotros. Desde el primer caso de estudio de este libro hemos utilizado archivos: archivos de texto como los que contienen el código Java, archivos html como los que contienen la documentación del programa, archivos mdl con los diagramas de clases, etc. Los directorios en donde guardamos los archivos con los datos y todos los de- más directorios que manejamos en los proyectos son a su vez archivos.
De manera general, podemos definir un archivo como una entidad que contiene información que puede ser almacenada en la memoria secundaria del computador (el disco duro o un CD). Todo archivo tiene un nombre que permite identificarlo de manera única dentro del computador, el cual está compuesto por dos partes: la ruta (path) y el nombre corto. La ruta describe la estructura de directorios dentro de los cuales se encuentra el archivo, empezando por el nombre de alguno de los discos duros del computador. Veamos en la siguiente tabla un ejemplo que ilustre lo anterior:
Nombre completo: | c:/dev/uniandes/cupi2/empleado/mundo/Empleado.java |
---|---|
Nombre corto: | Empleado.java |
Extensión o apellido: | .java |
Ruta o camino: | c:/dev/uniandes/cupi2/empleado/mundo/ |
El carácter '/' es llamado el separador de nombres de archivos (file separator). Este separador depende del sistema operativo en el que estemos trabajando. Por ejemplo, en Windows se suele utilizar como separador el carácter '\' (backslash) mientras que en Unix y Linux se utiliza el carácter '/' (slash).
La extensión que opcionalmente acompaña el nombre del archivo es una convención para indicar el tipo de información que hay dentro del archivo. El tipo de información dentro del archivo determina el programa con el que el archivo puede ser manipulado. Por ejemplo, los archivos de texto pueden ser manipulados por editores de texto, los archivos con extensión .xls deben ser manipulados por el programa Microsoft Excel, etc.
Desde nuestros programas en Java podemos acceder y leer información de los archivos del disco, siempre y cuando conozcamos su nombre para poder localizarlo y, además, conozcamos el tipo de información que el archivo contiene para poderla leer. Los archivos que manejaremos en nuestros programas tienen un formato especial que llamamos de propiedades (properties). Apoyándonos en algunas clases de utilidad que Java nos ofrece, vamos a poder leer información desde estos archivos de una manera muy sencilla.
Las clases Java que permiten manejar archivos desde un programa se encuentran definidas en el paquete java.io
, mientras que la clase que maneja las propiedades está en el paquete java.util
.
6.2. Leer Datos como Propiedades
Una propiedad se define como una pareja nombre = valor. Por ejemplo, para expresar en un archivo que la propiedad llamada campeonato.equipos tiene el valor 5, se usa la sintaxis:
campeonato.equipos = 5
En Java existe una clase llamada Properties que representa un conjunto de propiedades persistentes. Por persistentes queremos decir que estas propiedades pueden ser almacenadas en un archivo en memoria secundaria y leídas a la memoria del programa desde un archivo que ha sido escrito siguiendo las convenciones de nombre = valor. En la figura 6.9 se ilustra la correspondencia que queremos hacer entre un archivo llamado equipos.properties y un objeto de la clase Properties en memoria principal.
Fig. 6.9 Asociación entre un archivo y el objeto Properties en memoria principal |
---|
El archivo es un archivo de texto que contiene una lista de propiedades. Cada propiedad es una línea del archivo y está definida por un nombre, el operador =
y el valor de la propiedad (sin necesidad de comillas). Si en nuestro programa, el objeto de la clase Properties está referenciado desde una variable llamada pDatos
, una vez leído el archivo en memoria, podemos utilizar los métodos de dicha clase para obtener el valor de los elementos. Por ejemplo, si queremos saber el valor de la propiedad campeonato.nombre0, podemos utilizar el siguiente método, cuya respuesta será la cadena "A.C.Milan".
String nombre = pDatos.getProperty ( "campeonato.nombre0" );
Para completar el ejemplo, necesitamos aprender varias cosas. Primero necesitamos saber cómo localizar el archivo en el disco, luego hacer la asociación entre el archivo físico y un objeto en el programa que lo represente, y después, leer o cargar el contenido del archivo en el objeto Properties de nuestro programa. En las siguientes secciones veremos en detalle cada uno de estos pasos.
Por convención, para los nombres de las propiedades utilizamos una secuencia de palabras en minúsculas, separadas por un punto.
6.3. Escoger un Archivo desde el Programa
Como explicamos en la sección de definición de un archivo, el nombre físico de un archivo depende del sistema operativo en el que nuestro programa esté trabajando, en particular porque el carácter de separación de directorios puede cambiar entre los diferentes sistemas operativos. Por esta razón, para no depender del sistema operativo, en Java se puede hacer una abstracción de este nombre específico y convertirlo en un nombre independiente utilizando la clase File.
Para crear un objeto de la clase File que contenga la representación abstracta del archivo físico, debemos crear una instancia de dicha clase, usando la sintaxis que se muestra a continuación:
File archivoDatos = new File( "C:\n6_campeonato\data\equipos.properties" );
Si invocamos el constructor de la clase File con una cadena vacía (
null
), se disparará la excepción: java.lang.NullPointerException
La clase File nos ofrece varios servicios muy útiles, como métodos para saber si el archivo existe, preguntar por las características del archivo, crear un archivo vacío, renombrar un archivo y muchas otras más. En este nivel no las vamos a estudiar en detalle pero el lector interesado puede consultar la documentación de la clase.
Con la instrucción del ejemplo anterior, obtenemos una variable llamadaarchivoDatos
que está haciendo referencia a un objeto de la clase File que representa en abstracto el archivo que queremos leer. Lo anterior es suficiente si conocemos con anticipación el nombre del archivo de donde queremos cargar la información. Pero si, como en el caso de estudio, queremos que sea el cliente quien seleccione el archivo que quiere abrir, debemos utilizar otra manera de construir dicho objeto. Esto se ilustra en el ejemplo 6.
Ejemplo 6
Objetivo: Mostrar la manera de permitir al usuario escoger un archivo de manera interactiva.
En este ejemplo se presenta el código que permite a un programa preguntarle al usuario el archivo a partir del cual quiere leer alguna información.
public class InterfazCampeonato extends JFrame
{
public void cargarEquipos( )
{
...
}
}
- El método
cargarEquipos()
de la clase InterfazCampeonato es responsable de preguntar al usuario el archivo del cual quiere cargar la información del campeonato. - Veamos paso a paso la construcción de dicho método, comenzando por la manera de presentar la ventana de archivos disponibles en el computador y, luego, recuperar la selección que haya hecho el usuario.
public class InterfazCampeonato extends JFrame
{
public void cargarEquipos( )
{
...
JFileChooser fc = new JFileChooser( "./data" );
fc.setDialogTitle("Abrir archivo de campeonato");
...
}
...
}
- Lo primero que debemos hacer en el método es utilizar la clase FileChooser, que permite seleccionar un archivo. Creamos una instancia de dicha clase, pasándole en el constructor el directorio por el cual queremos comenzar la búsqueda de los archivos. En nuestro caso, indicamos que es el directorio llamado data.
- En la segunda instrucción de esta parte, cambiamos el título de la ventana.
public class InterfazCampeonato extends JFrame
{
public void cargarEquipos( )
{
...
File archivoCampeonato = null;
int resultado = fc.showOpenDialog( this );
if( resultado == JFileChooser.APPROVE_OPTION )
{
archivoCampeonato = fc.getSelectedFile( );
}
...
// Aquí debe ir la lectura del archivo
}
}
- Con el método
showOpenDialog
hacemos que la ventana de selección de archivos se abra. - Mientras el usuario no seleccione un archivo o cancele la operación, el método queda bloqueado en ese punto.
- El método
showOpenDialog
retorna un valor entero que describe el resultado de la operación. - Con el método
getSelectedFile
obtenemos el objeto de la clase File que describe el archivo escogido por el usuario (sólo si el usuario no canceló la operación).
El código del ejemplo 6 está incompleto, porque hasta ahora sólo hemos obtenido un objeto de la clase File que representa el archivo que el usuario quiere cargar en memoria. En la próxima sección veremos cómo realizar la lectura propiamente dicha.
6.4. Inicialización del Estado de la Aplicación
Para cargar el estado inicial del campeonato, debemos leer del archivo de propiedades la información sobre el número de equipos que van a participar (propiedad llamada "campeonato.equipos") y el nombre de los equipos (propiedades llamadas "campeonato.equipo" seguido de un índice que comienza en cero). Con dicha información podremos inicializar nuestro arreglo de equipos y, también, la matriz que representa la tabla de goles. El constructor de la clase Campeonato será el encargado de hacer esta inicialización, que vamos a dividir en tres subproblemas para los que hemos identificado tres metas intermedias:
- Meta 1: Cargar la información del archivo en un objeto Properties.
- Meta 2: Inicializar el arreglo de equipos con base en la información leída.
- Meta 3: Inicializar la matriz que representa la tabla de goles.
- La primera de estas metas se logra con los métodos explicados en el ejemplo 7.
Ejemplo 7
Objetivo: Mostrar la manera de crear un objeto de la clase Properties a partir de la información de un archivo.
En este ejemplo se muestra el código del constructor de la clase Campeonato, en términos de los métodos que resuelven cada una de las metas intermedias. Luego se muestra el método privado que logra la primera de ellas. Los demás métodos serán presentados más adelante.
public class Campeonato
{
//---------------------------------------
// Atributos
//---------------------------------------
private int maxEquipos;
private int[][] tablaGoles;
private Equipo[] equipos;
//---------------------------------------
// Constructor
//---------------------------------------
public Campeonato( File pArchivo ) throws Exception
{
Properties datos = cargarInfoCampeonato( pArchivo );
inicializarEquipos( datos );
inicializarTablaGoles( );
}
}
- El constructor recibe como parámetro el objeto de la clase File que describe el archivo con la información.
- Dicho objeto viene desde la interfaz del programa (obtenido con el método del ejemplo 6).
- El constructor lanza una excepción si encuentra un problema al leer el archivo o si el formato interno del mismo es inválido.
- El primer método carga la información del archivo en un objeto llamado datos.
- El segundo método recibe dicho objeto e inicializa el arreglo de equipos.
- El tercer método aprovecha la información dejada en los atributos, para crear la matriz con la tabla de goles.
private Properties cargarInfoCampeonato( File pArchivo ) throws Exception
{
Properties datos = new Properties( );
FileInputStream in = new FileInputStream( pArchivo );
try
{
datos.load( in );
in.close( );
}
catch( Exception e )
{
throw new Exception( "Formato inválido" );
}
return datos;
}
`
- Este método recibe un objeto de la clase File.
- Lo primero que hacemos es crear un objeto de la clase Properties (llamado
datos
) en el cual vamos a dejar el resultado del método. - Luego creamos un objeto de la clase FileInputStream que nos ayuda a hacer la conexión entre la memoria secundaria y el programa.
- La clase FileInputStream sirve para crear una especie de "canal" por donde los datos serán transmitidos. Para construir este objeto y asociarlo con el archivo seleccionado por el usuario, usamos el objeto de la clase File que recibimos como parámetro.
- Si el archivo referenciado por
pArchivo
no existe al tratar de crear la instancia de la clase FileInputStream se lanza una excepción. - Luego, usamos el método
load
de la clase Properties, pasándole como parámetro el "canal de lectura". Dicho método lanza una excepción si encuentra que el formato del archivo no es el esperado (no está formado por parejas de la forma nombre = valor). Allí atrapamos la excepción y la volvemos a lanzar con un mensaje signifucativo para nuestro programa. - Finalmente cerramos el "canal de lectura" con el método close.
private void inicializarTablaGoles( )
{
tablaGoles = new int[ maxEquipos ][ maxEquipos ];
for( int i = 0; i < maxEquipos; i++ )
{
for( int j = 0; j < maxEquipos; j++ )
{
if( i != j )
{
tablaGoles[ i ][ j ] = SIN_JUGAR;
}
else
{
tablaGoles[ i ][ j ] = INVALIDO;
}
}
}
}
- Este es el método que logra la tercera meta planteada en el constructor.
- Crea inicialmente una matriz que tiene una fila y una columna por cada equipo en el campeonato (es una matriz cuadrada).
- Luego inicializa cada una de las casillas de la matriz de enteros (patrón de recorrido total), usando para esto las constantes definidas en la clase.
- En la diagonal deja el valor INVALIDO.
6.5. Manejo de los Objetos de la Clase Properties
Para resolver la segunda meta, debemos implementar el método inicializarEquipos cuyo objetivo es inicializar el arreglo de equipos a partir de la información que recibe como parámetro de entrada. Para hacer esto necesitamos acceder al valor de las propiedades individuales que vienen en el objeto Properties. Esto se hace usando el método getProperty de la clase Properties, pasando como parámetro el nombre de la propiedad que queremos obtener (por ejemplo,"campeonato.equipos"). Veamos el código en el siguiente ejemplo.
Ejemplo 8
Objetivo: Mostrar la manera de acceder a las propiedades que forman parte de un objeto de la clase Properties.
En este ejemplo se muestra el código del método que implementa la segunda meta intermedia del constructor de la clase Campeonato.
private void inicializarEquipos( Properties pDatos )
{
String strNumeroEquipos = pDatos.getProperty( "campeonato.equipos" );
maxEquipos = Integer.parseInt( strNumeroEquipos );
equipos = new Equipo[ maxEquipos ];
for( int i = 0; i < maxEquipos; i++ )
{
String nombreEquipo = datos.getProperty( "campeonato.nombre" + i);
equipos[ i ]= new Equipo( nombreEquipo );
}
}
- Comenzamos obteniendo la propiedad que define el número de equipos del campeonato (llamada "campeonato.equipos"). El valor de una propiedad siempre es una cadena de caracteres.
- Luego, convertimos la respuesta que obtenemos en un entero, usando el método parseInt. Por ejemplo, convertimos la cadena "5" en el entero de valor 5. Note que dejamos el resultado en el atributo
maxEquipos
previsto para tal fin. - Creamos después el arreglo de equipos, reservando suficiente espacio para almacenar los objetos de la clase Equipo que van a representar cada uno de ellos.
- En un ciclo recuperamos los nombres de los equipos (a partir de las propiedades), y con esa información vamos creando los objetos de la clase Equipo que los representan y los vamos guardando secuencialmente en las casillas del arreglo.
- Los nombres de los equipos vienen en las propiedades "campeonato. nombre0", "campeonato.nombre1", etc., razón por la cual calculamos dicho nombre dentro del ciclo, agregando al final el índice en el que va la iteración.