jairogarcíarincón

LibretaDirecciones: Persistencia de datos con XML


4.32K



Introducción

En este momento, los datos de nuestra aplicación se pierden cada vez que la cerramos, reemplazándose por los valores del constructor de LibretaDirecciones.

Java nos permite, de una forma sencilla y en función del sistema operativo, guardar las preferencias de usuario de una aplicación, como por ejemplo, el último archivo abierto. No obstante, por defecto no está creada la entrada en el registro de Windows que nos va a permitir escribir en él, así que lo primero que haremos será crearla accediendo al regedit.exe y añadiendo la siguiente key:


Como usuario: HKEY_CURRENT_USER\Software\JavaSoft\Prefs
Como administrador: HKEY_LOCAL_MACHINE\Software\JavaSoft\Prefs


NOTA: Esto se podría implementar dentro de Java para que accediera al registro y añadiera la entrada si no existiera, pero para esta práctica no lo vamos a implementar. No obstante, se adjunta una solución de StackOverflow por si algún alumno la quiere implementar.

Una vez realizada esta operación, podemos hacer uso de la clase Preferences de Java para generar los siguientes métodos de lectura y escritura de las preferencias en LibretaDirecciones.java:


//Obtengo la ruta del archivo de la preferencias de usuario en Java
public File getRutaArchivoPersonas() {

Preferences prefs = Preferences.userNodeForPackage(LibretaDirecciones.class);
String rutaArchivo = prefs.get("rutaArchivo", null);
System.out.println(rutaArchivo);
if (rutaArchivo != null) {
return new File(rutaArchivo);
} else {
return null;
}
}

//Guardo la ruta del archivo en las preferencias de usuario en Java
public void setRutaArchivoPersonas(File archivo){

Preferences prefs = Preferences.userNodeForPackage(LibretaDirecciones.class);
if (archivo != null){
//Añado la ruta a las preferencias
prefs.put("rutaArchivo", archivo.getPath());
//Actualizo el título del escenario a partir del archivo
escenarioPrincipal.setTitle("Libreta de direcciones - "+archivo.getName());
}
else{
//Elimino la ruta de las preferencias
prefs.remove("rutaArchivo");
//Actualizo el título del escenario quitando el nombre del archivo
escenarioPrincipal.setTitle("Libreta de direcciones");
}

}


Persistencia mediante XML

Una vez que sabemos cómo leer y escribir en la preferencias, debemos elegir un formato de almacenamiento de datos.

Si bien en entornos conectados se recomendaría el uso de bases de datos tipo MySQL, para entornos locales y para poder almacenar copias de las libretas que creemos, vamos a utilizar el formato XML, ya que para nuestro proyecto es más que suficiente este tipo de almacenamiento.

La idea será que la aplicación guarde y lea de un archivo similar a este:


<personas>
<persona>
<fechaDeNacimiento>15-06-1974</fechaDeNacimiento>
<ciudad>Madrid</ciudad>
<nombre>Jairo</nombre>
<apellidos>García Rincón</apellidos>
<codigoPostal>28440</codigoPostal>
<direccion>Mi dirección</direccion>
</persona>
<persona>
<fechaDeNacimiento>12-07-1990</fechaDeNacimiento>
<ciudad>Albacete</ciudad>
<nombre>Luís</nombre>
<apellidos>López Pérez</apellidos>
<codigoPostal>05431</codigoPostal>
<direccion>Otra dirección</direccion>
</persona>
</personas>


JAXB

Para ello, usaremos la librería de Java JAXB (Java Arquitecture XML Biilding), incluida en el JDK, y que nos permitirá tanto convertir objetos Java en XML (marshalling) como convertir XML en objetos Java (unmarshalling).

Los datos que queremos guardar están en la variable datosPersona (dentro de LibretaDirecciones), que es de tipo ObservableList.

Como JAXB requiere que la clase raíz (la que contenga a todo el árbol XML) sea anotada anotada con @XmlRootElement, debemos crear un modelo diferente contener nuestra lista de personas (Persona) de cara a ser adaptada a XML por JAXB.

Para ello, vamos a crear un nuevo modelo llamado Empaquetador.java dentro del paquete model con el siguiente código:


package model;

import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "personas") //Define el nombre del elemento raíz XML
public class Empaquetador {

private List<Persona> personas;

@XmlElement(name = "persona") //Opcional para el elemento especificado
public List<Persona> getPersonas(){
return personas;
}

public void setPersonas(List<Persona> personas){
this.personas = personas;
}

}


Una vez que tenemos nuestro Empaquetador.java, ya podemos utilizarlo dentro de la clase principal LibretaDirecciones.java para generar los métodos que se encarguen de leer (unmarshalling) y escribir (marshalling) datos XML.


//Cargo personas de un fichero
public void cargaPersonas(File archivo){

try {
//Contexto
JAXBContext context = JAXBContext.newInstance(Empaquetador.class);
Unmarshaller um = context.createUnmarshaller();

//Leo XML del archivo y hago unmarshall
Empaquetador empaquetador = (Empaquetador) um.unmarshal(archivo);

//Borro los anteriores
datosPersona.clear();
datosPersona.addAll(empaquetador.getPersonas());

//Guardo la ruta del archivo al registro de preferencias
setRutaArchivoPersonas(archivo);

} catch (Exception e) {
//Muestro alerta
Alert alerta = new Alert(Alert.AlertType.ERROR);
alerta.setTitle("Error");
alerta.setHeaderText("No se pueden cargar datos de la ruta "+ archivo.getPath());
alerta.setContentText(e.toString());
alerta.showAndWait();

}

}

//Guardo personas en un fichero
public void guardaPersonas(File archivo) {

try {
//Contexto
JAXBContext context = JAXBContext.newInstance(Empaquetador.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

//Empaqueto los datos de las personas
Empaquetador empaquetador = new Empaquetador();
empaquetador.setPersonas(datosPersona);

//Marshall y guardo XML a archivo
m.marshal(empaquetador, archivo);

//Guardo la ruta delk archivo en el registro
setRutaArchivoPersonas(archivo);

} catch (Exception e) { // catches ANY exception
//Muestro alerta
Alert alerta = new Alert(Alert.AlertType.ERROR);
alerta.setTitle("Error");
alerta.setHeaderText("No se puede guardar en el archivo "+ archivo.getPath());
alerta.setContentText(e.toString());
alerta.showAndWait();
}
}


Acciones del menú

Una vez implementados estos métodos, ya podríamos asociarlos a acciones en nuestro menú superior. Para ello, modifica VistaPrincipal.fxml para que el primer Menu se llame Archivo e incluya los MenuItem siguientes: Nuevo, Abrir, Guardar como.., Guardar y Salir. Añade también un Menu llamado Acerca de con un MenuItem llamado Mostrar:

Si lo deseas, con la opción Accelerator de la vista Properties puedes establecer atajos de teclado para lanzar las acciones asociadas a los MenuItem.

Para poder generar las acciones correspondientes de cada MenuItem y ya que estamos controlando la vista principal, crearemos una nueva clase llamada VistaPrincipalController dentro del paquete view con el siguiente código:


public class VistaPrincipalController {

//Referencia a la clase principal
private LibretaDirecciones libretaDirecciones;

//Es llamada por la clase Principal para tener una referencia de vuelta de si misma
public void setLibretaDirecciones(LibretaDirecciones libretaDirecciones) {
this.libretaDirecciones = libretaDirecciones;
}

//Creo una nueva libreta de direcciones en XML vacía
@FXML
private void nuevo() {
libretaDirecciones.getDatosPersona().clear();
libretaDirecciones.setRutaArchivoPersonas(null);
}

//Abro un File Chooser para que el usario seleccione una libreta
@FXML
private void abrir() {
FileChooser fileChooser = new FileChooser();

//Filtro para la extensión
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
"XML files (*.xml)", "*.xml");
fileChooser.getExtensionFilters().add(extFilter);

//Muestro el diálogo de guardar
File archivo = fileChooser.showOpenDialog(libretaDirecciones.getPrimaryStage());

if (archivo != null) {
libretaDirecciones.cargaPersonas(archivo);
}
}

//Guardar
@FXML
private void guardar() {
File archivo = libretaDirecciones.getRutaArchivoPersonas();
if (archivo != null) {
libretaDirecciones.guardaPersonas(archivo);
} else {
guardarComo();
}
}

//Abro un File Chooser para guardar como
@FXML
private void guardarComo() {

FileChooser fileChooser = new FileChooser();

//Filtro para la extensión
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
"XML files (*.xml)", "*.xml");
fileChooser.getExtensionFilters().add(extFilter);

//Muestro el diálogo de guardar
File archivo = fileChooser.showSaveDialog(libretaDirecciones.getPrimaryStage());

if (archivo != null) {
//Me aseguro de que tiene la extensión correcta
if (!archivo.getPath().endsWith(".xml")) {
archivo = new File(archivo.getPath() + ".xml");
}
libretaDirecciones.guardaPersonas(archivo);
}
}

//Acerca de
@FXML
private void acercaDe() {
//Muestro alerta
Alert alerta = new Alert(Alert.AlertType.INFORMATION);
alerta.setTitle("Acerca de");
alerta.setContentText("Autor: Marco Jakob\nWebsite: http://code.makery.ch\nAdaptación 2018: Jairo García Rincón");
alerta.showAndWait();
}

//Salir
@FXML
private void salir() {
System.exit(0);
}

}


Si prestamos atención a los métodos que usan la clase FileChooser veremos que primero se crea una nueva instancia de la clase FileChooser. A continuación, se le añade un filtro de extensión para que sólo se muestren los archivos terminados en .xml y por último, el objeto FileChooser se muestra justo encima de la escena principal.

Si el usuario cierra la ventana del FileChooser sin escoger un archivo, se devuelve null. En cualquier otro caso, se obtiene el archivo seleccionado, y se lo podemos pasar al método cargaPersonas(...) o al método guardaPersonas(...) de la clase principal LibretaDirecciones.

Lo único que nos quedaría sería asociar esas acciones a los MenuItem. Para ello, abrimos VistaPrincipal.fxml con Scene Builder, le asociamos el controlador VistaPrincipalController (parte izquierda, sección Controller) y a cada MenuItem le asociamos su acción determinada (parte derecha, sección Code, campo On Action).

Conexión del controlador con la clase principal

Como en varios sitios VistaPersonaController va a tener que hacer uso de la clase principal LibretaDirecciones, necesita una referencia a ella. Para conseguirlo, modificaremos el método initLayoutPrincipal() de LibretaDirecciones para que incluya dicha referencia.

Además, he añadido unas líneas al final que permiten recuperar y cargar el último archivo abierto por la aplicación (si lo hay).


public void initLayoutPrincipal(){

//Cargo el layout principal a partir de la vista VistaPrincipal.fxml
FXMLLoader loader = new FXMLLoader();
URL location = LibretaDirecciones.class.getResource("../view/VistaPrincipal.fxml");
loader.setLocation(location);
try {
layoutPrincipal = loader.load();
} catch (IOException ex) {
Logger.getLogger(LibretaDirecciones.class.getName()).log(Level.SEVERE, null, ex);
}

//Cargo la escena que contiene ese layout principal
Scene escena = new Scene(layoutPrincipal);
escenarioPrincipal.setScene(escena);

//Doy al controlador acceso a la aplicación principal
VistaPrincipalController controller = loader.getController();
controller.setLibretaDirecciones(this);

//Muestro la escena
escenarioPrincipal.show();

//Intento cargar el último archivo abierto
File archivo = getRutaArchivoPersonas();
if (archivo != null){
cargaPersonas(archivo);
}

}


El problema de las fechas

Si ahora ejecutamos la aplicación, deberíamos ser capaces de guardar y leer personas en ficheros XML.

El problema es que si analizamos el archivo XML, veremos que las fechas aparecen vacías. Esto es debido a que JAXB no sabe como convertir LocalDate a XML. Debemos por tanto generar un adaptador a medida que realice esta conversión.

Genera una nueva clase llamado AdaptadorDeFechas con el siguiente código:


public class AdaptadorDeFechas extends XmlAdapter<String, LocalDate>{

@Override
public LocalDate unmarshal(String v) throws Exception {
return LocalDate.parse(v);
}

@Override
public String marshal(LocalDate v) throws Exception {
return v.toString();
}

}


A continuación, abre la clase Persona y añade la siguiente anotación al método getFechaDeNacimiento():


@XmlJavaTypeAdapter(AdaptadorDeFechas.class)
public LocalDate getFechaDeNacimiento() {
return fechaDeNacimiento.get();
}


Ahora el archivo XML se debería guardar y leer sin problema y con el formato de fechas adecuado.

Resumen

A modo de resumen, vamos a analizar cómo funciona el conjunto:

  • La aplicación se inicia con la ejecución del método main(...) de la clase LibretaDirecciones.
  • El constructor public LibretaDirecciones() es invocado y añade algunos datos de ejemplo.
  • El método start(...) de la clase LibretaDirecciones es invocado, el cual a su vez invoca a initLayoutPrincipal() para iniciar la vista principal utilizando el archivo VistaPrincipal.fxml. El archivo FXML tiene información sobre qué controlador utilizar y enlaza la vista con su controlador VistaPrincipalController.
  • LibretaDirecciones obtiene el controlador VistaPrincipalController del cargador FXML y le pasa a ese controlador una referencia a sí mismo. Con esta referencia el controlador puede acceder a los métodos (públicos) de LibretaDirecciones.
  • Al final del método initLayoutPrincipal() se intenta obtener el último archivo de direcciones abierto desde las Preferences. Si existe esa información se leen los datos del XML. Estos datos sobre-escribirán los datos de ejemplo generados en el constructor.


Publicado el 30 de Enero de 2025

xmlinterfacesjavafx