Mostrar otros tipos de datos 

En la clase controladora de la vista detalle ya se ha implementado previamente el código necesario para mostrar los datos de tipo String (concretamente en el método mostrarDatos()), que, como se pudo ver, tan sólo consiste en asignar al método setText del TextField el valor de tipo String retornado por el método get correspondiente. Por ejemplo:

textFieldNombre.setText(persona.getNombre());

Pero en el caso de propiedades que no sean de tipo String la cosa se complica.  Veremos algunos casos, basados en los diferentes tipos de datos que se han usado en la tabla Persona de la base de datos, donde se han creado distintos tipos de columnas con este fin.

Datos numéricos

En la tabla Persona de la base de datos se han creado las siguientes columnas de tipo numérico:

NUM_HIJOS SMALLINT
SALARIO DECIMAL(7,2)

Al generar la clase entidad Persona se han transformado en las siguientes propiedades de Java:

private Short numHijos;
private BigDecimal salario;

Así, por ejemplo, el método getNumHijos retorna un tipo Short, por lo que no se podrá mostrar directamente en un TextField. Hay que buscar la manera de convertir los objetos Short y BigDecimal (en este ejemplo) a String. Para ello esas clases numércias ofrecen el método toString() que hace esa labor. Un factor a tener en cuenta es que no haya ningún valor asignado a esas propiedades, en cuyo caso se obtendría un objeto null, sobre el que no se puede utilizar ningún método (como el toString()), por lo que hay que controlar esa posible situación.

if(persona.getNumHijos() != null) {
    textFieldNumHijos.setText(persona.getNumHijos().toString());
}
if(persona.getSalario() != null) {
    textFieldSalario.setText(persona.getSalario().toString());
}

Valores de tipo boolean

La propiedad jubilado, que se encuentra declarada en la clase Persona como de tipo Boolean, al igual que en la base de datos, se va a mostrar en la vista detalle usando un control CheckBox, de manera que aparecerá seleccionado si dicha propiedad contiene el valor true.

private Boolean jubilado;

Al igual que en el caso anterior, hay que asegurarse de que el valor de la propiedad de tipo Boolean contenga algún dato, es decir, que no sea null. Hay que recordar que no es lo mismo una variable o propiedad del tipo básico boolean que una que almacene un objeto Boolean (observa las mayúsculas), ya que el tipo básico boolean sólo puede almacenar los valores true o false, mientras que una variable o propiedad de la clase Boolean puede contener los objetos true, false o ninguno (null).

if (persona.getJubilado() != null) {
    checkBoxJubilado.setSelected(persona.getJubilado());
}

Observa que se ha aprovechado que la llamada al método persona.getJubilado() retorna un objeto Boolean para indicar directamente si el CheckBox debe estar seleccionado o no, ya que a su método setSelected hay que indicarle un parámetro de tipo boolean.

Datos de opción múltiple

Para el caso del estado civil de la persona (que en la aplicación se ha establecido que sólo pueda ser Soltero, Casado o Viudo por simplificar), se han incluido en la vista de detalle 3 controles de tipo RadioButton

En la base de datos se ha declarado su columna correspondiente como CHAR(1), lo cual ha generado la propiedad asociada de tipo Character. Ese carácter va a ser 'S' para el estado civil soltero, 'C' para casado, o 'V' para viudo.

private Character estadoCivil;

Para asociar claramente en el código fuente de la clase controladora de la vista detalle cada tipo de estado con su carácter correspodiente, es conveniente declarar una serie de constantes, de manera que permitan indicar el nombre de la constante en lugar de simplemente el carácter cada vez que sea necesario su uso.

public static final char CASADO = 'C';
public static final char SOLTERO = 'S';
public static final char VIUDO = 'V';

Ahora simplemente habrá que comprobar con cuál de esos valores coincide el valor almacenado en la propiedad estadoCivil del objeto Persona, para seleccionar un determinado RadioButton.

if (persona.getEstadoCivil() != null) {
    switch (persona.getEstadoCivil()) {
        case CASADO:
            radioButtonCasado.setSelected(true);
            break;
        case SOLTERO:
            radioButtonSoltero.setSelected(true);
            break;
        case VIUDO:
            radioButtonViudo.setSelected(true);
            break;
    }
}

Datos de tipo fecha

La propiedad fechaNacimiento se encuentra declarada en la clase Persona como un objeto de la clase Date (del paquete java.util), y su valor se desea mostrar en un control DatePicker de JavaFX. Para asignar un valor a un control DatePicker se debe usar su método setValue(), el cual sólo admite como parámetro un objeto de LocalDate. Hay que buscar la manera de realizar esa conversión de Date a LocalDate, lo que se puede realizar con el código siguiente que puede obtenerse haciendo una búsqueda en Internet.

if (persona.getFechaNacimiento() != null) {
    Date date = persona.getFechaNacimiento();
    Instant instant = date.toInstant();
    ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
    LocalDate localDate = zdt.toLocalDate();
    datePickerFechaNacimiento.setValue(localDate);
}

Objetos de una tabla relacionada

En el ejemplo usado en este tutorial, cada objeto Persona está relacionado con un objeto Provincia, siguiendo la relación que se había establecido en la base de datos. Para indicar qué provincia está asociada a la persona que se esté mostrando en la vista de detalle, se va a utilizar una lista desplegable (ComboBox), donde además de mostrar la provincia que actualmente tiene asignada, se podrá seleccionar cualquier otra provincia de las que se encuentren en la tabla Provincia de la base de datos.

Para que la lista desplegable contenga todas las provincias que se encuentren almacenadas en la base de datos, hay que hacer una consulta para obtener todos los registros de la tabla Provincia y asignárselos a la lista desplegable.

Query queryProvinciaFindAll = entityManager.createNamedQuery("Provincia.findAll");
List listProvincia = queryProvinciaFindAll.getResultList();
comboBoxProvincia.setItems(FXCollections.observableList(listProvincia));

Ya que el ComboBox va a contener una lista de objetos de clase Provincia, hay que completar la información correspondiente en la declaración del ComboBox, que se encuentra en la parte superior del código, y que ha sido generado automáticamente cuando se hizo el Make Controller. Deberás cambiar la declaración:

private ComboBox<?> comboBoxProvincia;

Por esta otra, donde se especifica el tipo de contenido del ComboBox:

private ComboBox<Provincia> comboBoxProvincia;

En caso de que el objeto Persona tenga asignada alguna Provincia, se deberá seleccionar para que se muestre directamente en el ComboBox.

if (persona.getProvincia() != null) {
    comboBoxProvincia.setValue(persona.getProvincia());
}

Ya que este ComboBox va a contener objetos (Provincia) y no simples Strings, hay que determinar cómo queremos que se muestren en la lista dichos objetos. Es decir, si queremos que aparezca únicamente el nombre de la provincia, sólo su código, una combinación del código y el nombre, etc. Para este ejemplo vamos a tratar de mostrar siempre el código junto con el nombre, por ejemplo, 11-Cádiz. Para ello, hay que asignar al ComboBox un CellFactory, en cuyo método updateItem se debe asignar (con setText()) el String que se desea que aparezca por cada objeto de la lista del ComboBox.

comboBoxProvincia.setCellFactory((ListView<Provincia> l) -> new ListCell<Provincia>() {
    @Override
    protected void updateItem(Provincia provincia, boolean empty) {
        super.updateItem(provincia, empty);
        if (provincia == null || empty) {
            setText("");
        } else {
            setText(provincia.getCodigo() + "-" + provincia.getNombre());
        }
    }
});

Además hay que establecer cómo se va a mostrar el elemento que aparece cuando la lista del ComboBox está cerrada, es decir, el elemento que se encuentre seleccionado en un momento determinado. Lo normal es que se muestre igual que en la lista, pero se podría establecer otro formato diferente si se desea. La manera en qué se mostrará se debe establecer asignando un objeto Converter al ComboBox usando el método setConverter. Dentro de ese Converter se deben definir los métodos toString y fromString, para establecer cómo se debe convertir cada objeto Persona (en este caso) a String y viceversa si fuera necesario. Para el caso actual, sólo es necesario implementar el método toString retornando un String con el formato deseado.

comboBoxProvincia.setConverter(new StringConverter<Provincia>() {
    @Override
    public String toString(Provincia provincia) {
        if (provincia == null) {
            return null;
        } else {
            return provincia.getCodigo() + "-" + provincia.getNombre();
        }
    }
    @Override
    public Provincia fromString(String userId) {
        return null;
    }
});

Imágenes

Para las imágenes asociadas a cada objeto Persona se va a establecer que siempre se encuentren alojadas en una carpeta denominada Fotos en la misma carpeta donde se encuentre la aplicación. Así que en la propiedad foto sólo se almacenará el nombre del archivo correspondiente a la imagen.

Parece conveniente crear una constante que almacene el nombre de la carpeta donde se alojarán las imágenes, ya que habrá que hacer referencia a ese nombre de carpeta en distintos lugares del código fuente, y así se evitan errores como que se indiquen nombres diferentes en distintas partes del código fuente.

public static final String CARPETA_FOTOS = "Fotos";

En el momento de mostrar la imagen correspondiente al nombre de archivo que se encuentre en la propiedad foto del objeto Persona, se comprobará si realmente existe dicho archivo. Si no existiera se mostrará un mensaje informativo, y si existe se cargará en el control de tipo ImageView que se colocó en la vista de detalle.

if (persona.getFoto() != null) {
    String imageFileName = persona.getFoto();
    File file = new File(CARPETA_FOTOS + "/" + imageFileName);
    if (file.exists()) {
        Image image = new Image(file.toURI().toString());
        imageViewFoto.setImage(image);
    } else {
        Alert alert = new Alert(AlertType.INFORMATION, "No se encuentra la imagen");
        alert.showAndWait();
    }
}

Guardar otros tipos de datos

En un artículo anterior también se comentó la manera de actualizar las propiedades del objeto Persona que fueran de tipo String, a partir de la información introducida por el usuario en un TextField. Por ejemplo, recordemos:

persona.setNombre(textFieldNombre.getText());

También se comentó cómo se actualiza la información en la base de datos, así que veamos ahora cómo actualizar propiedades del objeto Persona que no sean de tipo String, para completar el código fuente asociado al botón Guardar de la clase controladora de la vista detalle a partir de los datos indicados por el usuario en la vista de detalle creada para JavaFX anteriormente.

En muchos casos será conviente comprobar que el dato introducido es correcto, de manera que no se permita guardar los cambios mientras los datos no sean correctos. Para ello se puede declarar una variable booleana en el inicio del código del botón Guardar, donde se almacenará si el formato de los datos introducidos es correcto o no.

boolean errorFormato = false;

Esto conlleva que todo el código que anteriormente se había escrito para almacenar el objeto en la base de datos debe encerrarse en una sentencia if donde se comprueba que finalmente los datos introducidos son correctos. Además, hay que comprobar que se cumplimentan los datos que se han indicado como obligatorios en la base de datos (NOT NULL), y que no se excede la longitud establecidad para los columnas, por ejemplo, el tamaño de los VARCHAR. En esos casos, al intentar hacer el volcado a la base de datos se producirá una excepción de tipo RollbackException, así que se debe incluir una sentencia try-catch que capture esa posible excepción mostrando un mensaje al usuario para que arregle los problemas encontrados.

// Recoger datos de pantalla
if(!errorFormato) {  // Los datos introducidos son correctos
    try {
       // Aquí va el código para guardar el objeto en la base de datos
       //    y ocultar la vista actual
    } catch (RollbackException ex) { // Los datos introducidos no cumplen los requisitos de la BD
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setHeaderText("No se han podido guardar los cambios. "
                + "Compruebe que los datos cumplen los requisitos");
        alert.setContentText(ex.getLocalizedMessage());
        alert.showAndWait();
    }
}

Datos numéricos

En el ejemplo propuesto, el usuario puede indicar el número de hijos de la persona que se esté editando, para lo que se ofrece en pantalla un TextField donde el usuario debe indicar un valor numérico que se almacenará en la propiedad numHijos de tipo Short. El código que efectúe esa operación debe trasformar el valor introducido por el usuario en el TextField, que se recogerá siempre como String, convirtiéndolo en un objeto Short. Esto se puede hacer con el método Short.valueOf().

Hay que tener en cuenta además que el usuario ha podido introducir caracteres no numéricos, lo cual producirá una excepción de tipo NumberFormatException al intentar hacer la conversión con el valueOf() anterior. En ese caso se mostrará un mensaje al usuario para que lo arregle, y se asignará el valor true a la variable errorFormato para evitar que se almacene la Persona en la base de datos. Además se mantendrá el foco en el mismo TextField usando el método requestFocus().

if(!textFieldNumHijos.getText().isEmpty()) {
    try {
        persona.setNumHijos(Short.valueOf(textFieldNumHijos.getText()));
    } catch(NumberFormatException ex) {
        errorFormato = true;
        Alert alert = new Alert(AlertType.INFORMATION, "Número de hijos no válido");
        alert.showAndWait();
        textFieldNumHijos.requestFocus();
    }
}

También se utiliza en el ejemplo valores de tipo numérico con decimales para almacenar el salario. La asignación de la propiedad correspondiente a partir del datos obtenido desde un TextField se realiza de manera similar, aunque en este caso se usa BigDecimal.valueOf() para la conversión, ya que la propiedad salario es de tipo BigDecimal y en la base de datos es DECIMAL

if(!textFieldSalario.getText().isEmpty()) {
    try {
        persona.setSalario(BigDecimal.valueOf(Double.valueOf(textFieldSalario.getText()).doubleValue()));
    } catch(NumberFormatException ex) {
        errorFormato = true;
        Alert alert = new Alert(AlertType.INFORMATION, "Salario no válido");
        alert.showAndWait();
        textFieldSalario.requestFocus();
    }    
}

Datos booleanos

Una manera cómoda de que el usuario indique un valor de tipo boolean es ofreciéndole un control del tipo CheckBox en la vista. Así se ha diseñado para la propiedad jubilado. La clase CheckBox ofrece el método isSelected() que retorna un boolean con valor true si está seleccionado o false en caso contrario. Por tanto se puede usar directamente ese valor retornado para asignárselo a la propiedad jubilado de Persona

persona.setJubilado(checkBoxJubilado.isSelected());

Valores de opción múltiple

Se ha establecido que la propiedad estadoCivil de Persona almacene un carácter para indicar el valor correspondiente, para lo que se habían creado antes una serie de constantes. En la vista detalle se había incluido un RadioButton para cada posible estado, por lo que habrá que comprobra cuál está seleccionado para asignar el carácter correspondiente. 

if(radioButtonCasado.isSelected()) {
    persona.setEstadoCivil(CASADO);
} else if(radioButtonSoltero.isSelected()) {
    persona.setEstadoCivil(SOLTERO);
} else if(radioButtonViudo.isSelected()) {
    persona.setEstadoCivil(VIUDO);
}

Fechas

Ya se comentó anteriormente que hay que hacer una conversión del tipo Date que se utiliza para la propiedad fechaNacimiento de Persona para mostrarla en el control DatePicker de la pantalla. Ahora habrá que hacer la conversión inversa, desde DatePicker a Date de una manera similar.

if(datePickerFechaNacimiento.getValue() != null) {
    LocalDate localDate = datePickerFechaNacimiento.getValue();
    ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
    Instant instant = zonedDateTime.toInstant();
    Date date = Date.from(instant);
    persona.setFechaNacimiento(date);
} else {
    persona.setFechaNacimiento(null);
}

Objetos de tabla relacionada

El objeto que el usuario haya seleccionado en la lista desplegable (ComboBox) se puede asignar directamente. El método getValue() del ComboBox retornará dicho objeto, que se puede asignar directamente a la propiedad provincia utilizada en este ejemplo. 

if(comboBoxProvincia.getValue() != null) {
    persona.setProvincia(comboBoxProvincia.getValue());
} else {
    Alert alert = new Alert(AlertType.INFORMATION, "Debe indicar una provincia");
    alert.showAndWait();
    errorFormato = true;
}

Imagen

Para que el usuario pueda cambiar la imagen asociada a la propiedad foto de Persona, se había incluido un boton Examinar que debe permitirle buscar una imagen entre los archivos de su equipo. El código asociado al evento onAction de ese botón será como el mostrado a continuación.

En este código se crea un objeto File asociado a la carpeta donde se alojarán las imágenes que seleccione el usuario. Se usa la constante CARPETA_FOTOS que se declaró anteriormente. Si no existiera dicha carpeta, la aplicación la creará con la llamada a mkdir().

A continuación se crea un objeto FileChooser que muestra al usuario una ventana de diálogo donde puede elegir el archivo que desee. Este objeto FileChooser se configura de manera que sólo se muestren inicialmente los archivos con extensiones jpg o png, aunque también se ofrece la posibilidad de mostrar todos los archivos. La llamada al método showOpenDialog() mostrará esa ventana y retornará el objeto File correspondiente al archivo que haya seleccionado el usuario o null si no ha seleccionado ninguno.

El archivo que haya seleccionado se copiará a la carpeta CARPETA_FOTOS y el nombre del archivo se almacena en la propiedad foto del objeto Persona. Además se muestra en el control ImageView de la vista.

Si el archivo seleccionado ya existiera con ese nombre en la carpeta de fotos, al intentar hacer la copia con Files.copy() se producirá una excepción de tipo FileAlreadyExistsException, y si se produce algún error que impida hacer la copia (por ejemplo, que la carpeta no tenga permisos de escritura) se producirá una excepción IOException. En ambos casos se muestra un mensaje al usuario para informarle. 

@FXML
private void onActionButtonExaminar(ActionEvent event) {
    File carpetaFotos = new File(CARPETA_FOTOS);
    if(!carpetaFotos.exists()) {
        carpetaFotos.mkdir();
    }
    FileChooser fileChooser = new FileChooser();
    fileChooser.setTitle("Seleccionar imagen");
    fileChooser.getExtensionFilters().addAll(
            new FileChooser.ExtensionFilter("Imágenes (jpg, png)", "*.jpg", "*.png"),
            new FileChooser.ExtensionFilter("Todos los archivos", "*.*")
        );
    File file = fileChooser.showOpenDialog(rootPersonaDetalleView.getScene().getWindow());
    if(file != null) {
        try {
            Files.copy(file.toPath(), new File(CARPETA_FOTOS + "/"+file.getName()).toPath());
            persona.setFoto(file.getName());
            Image image = new Image(file.toURI().toString());
            imageViewFoto.setImage(image);
        } catch (FileAlreadyExistsException ex) {
            Alert alert = new Alert(AlertType.WARNING, "Nombre de archivo duplicado");
            alert.showAndWait();
        } catch (IOException ex) {
            Alert alert = new Alert(AlertType.WARNING, "No se ha podido guardar la imagen");
            alert.showAndWait();
        }
    }
}

Con el fin de que el usuario pueda dejar al objeto Persona sin una foto en caso de que anteriormente se le hubiera asignado alguna, se va a incluir también un botón Suprimir foto, cuyo código del evento onAction será como el que se muestra a continuación.

En primer lugar se solicitará al usuario una confirmación antes de hacer el borrado de la imagen, ya que puede que haya pulsado el botón sin intención. En esa ventana de confrmación se le preguntará si sólo desea dejar a la persona sin foto, o si además se desea eliminar el archivo de la carpeta de fotos. En caso de que se desee eliminar el archivo se usará el método delete().

Para dejar al objeto persona sin una foto asociada de indicará el valor null a la propiedad foto, y el control ImageView de la vista se deja también sin imagen.

@FXML
private void onActionSuprimirFoto(ActionEvent event) {
    Alert alert = new Alert(AlertType.CONFIRMATION);
    alert.setTitle("Confirmar supresión de imagen");
    alert.setHeaderText("¿Desea SUPRIMIR el archivo asociado a la imagen, \n"
            + "quitar la foto pero MANTENER el archivo, \no CANCELAR la operación?");
    alert.setContentText("Elija la opción deseada:");

    ButtonType buttonTypeEliminar = new ButtonType("Suprimir");
    ButtonType buttonTypeMantener = new ButtonType("Mantener");
    ButtonType buttonTypeCancel = new ButtonType("Cancelar", ButtonData.CANCEL_CLOSE);

    alert.getButtonTypes().setAll(buttonTypeEliminar, buttonTypeMantener, buttonTypeCancel);

    Optional<ButtonType> result = alert.showAndWait();
    if (result.get() == buttonTypeEliminar){
        String imageFileName = persona.getFoto();
        File file = new File(CARPETA_FOTOS + "/" + imageFileName);
        if(file.exists()) {
            file.delete();
        }
        persona.setFoto(null);
        imageViewFoto.setImage(null);
    } else if (result.get() == buttonTypeMantener) {
        persona.setFoto(null);
        imageViewFoto.setImage(null);
    } 
}