Evitando que un error detenga un Timer

Error, pero el programa continua funcionando
Error, pero el programa continua funcionando

La razón mas común por la que usuaria un Timer en su aplicación Java es el tener funciones corriendo en segundo plano, usualmente para transmitir o recibir datos en forma periódica durante un periodo prolongado, usualmente todo el día y sin demasiada vigilancia por parte de un usuario y esto implica preparar su aplicación para que una excepción no trabe o cierra la aplicación, por que le garantizo eso siempre pasara en el peor momento posible.

¿Que detiene un Timer?

El Timer se detiene en el caso de que una excepción no manejada ocurra dentro del método run del TimerTask siendo ejecutado por el Timer, esto es una excepción que no tenga un catch para manejarla, esta excepción saltara mas arriba en su código y en el proceso detendrá el Timer.

¿Como nos afecta esto? bueno recuerde que aunque el compilador le exige capturar algunas excepciones hay varias que pueden aparecer sin avisar como CommunicationsException (la cual aparece si la conexion a una base de datos se pierde por causa de la red) o un error al convertir un JSON a objeto, o una excepción aritmética resultado de una división con mas decimales de lo esperado.

Por fortuna prevenir esto es muy sencillo.

Prevenir que una excepción detenga el Timer.

La mejor forma de prevenir el Timer de ser detenido por una excepción es capturar todas las excepciones posibles y aunque eso es posible capturando cualquier objeto de la clase o subclase Exception se recomienda aprovechar que cada excepción significa algo diferente y excepto algunas cosas como fallos de red o de conexión a base de datos seguro habrá varios casos que pueda corregir

Pero suficiente teoría pasemos a un ejemplo.

Ejemplo.

Usaremos de ejemplo un caso que seguramente le a pasado, que la conexión a una base de datos ser pierda por un momento, esto lo simularemos parando el servicio MySQL mientras el programa corre.

package mx.hashCode.TimerError;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TimerSQL extends TimerTask {
    static private final Logger LOGGER = Logger.getLogger("mx.hashCode.TimerError.TimerSQL");

    public Connection crearConexion() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        String usuario = "archivista";
        String passwd = "123456789";

        Connection cx = DriverManager.getConnection ("jdbc:mysql://127.0.0.1:3306/"+ "?useSSL=false&" + "user="+ usuario + "&" + "password=" + passwd + "");

        return cx;
    }

    @Override
    public void run() {
        try {
            String sql = "SELECT * FROM pruebas.juegos;";            
            
            try(Connection cx = this.crearConexion(); Statement consulta = cx.createStatement()) {
                ResultSet data = consulta.executeQuery(sql);

                LOGGER.log(Level.INFO, "Presentaremos los datos");
                System.out.println("");
                while(data.next() == true) {
                    System.out.print(data.getString("nombre") + " ");
                    System.out.println(data.getString("consola"));
                }
                System.out.println("");
                data.close();
            }

        } catch (SQLException | ClassNotFoundException e) {
            LOGGER.log(Level.SEVERE, "OCURRIO UN ERROR DE ACCESO A DB!!!");
            LOGGER.log(Level.SEVERE, "Pero el ciclo deberia repetirse sin problema");
            //LOGGER.log(Level.SEVERE, null, e);
        }
    }

}
package mx.hashCode.TimerError;

import java.util.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Hello world!
 *
 */
public class App 
{
    private static final Logger LOGGER = Logger.getLogger("mx.hashCode.TimerError.App");

    public static void main( String[] args )
    {
        LOGGER.log(Level.INFO, "Inicializamos el timer");
        Timer timer = new Timer();
        TimerSQL timerSQL = new TimerSQL();

        timer.schedule(timerSQL, 0, 1000*10);
    }
}

Esta es la tabla que consulta el programa.

CREATE TABLE `juegos` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `consola` varchar(100) DEFAULT NULL,
  `nombre` varchar(100) DEFAULT NULL,
  `obtenido` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1

Obviamente deberá modificar los datos de conexión poner algún que otro dato en la tabla, hecho eso corra el programa con el servicio MySQL activo y vera la siguiente salida

Salida Correcta
Salida Correcta

Ahora desactive el servicio MySQL (esto variara dependiendo de su sistema operativo) y vera

Salida sin MySQL
Salida sin MySQL

Y sin embargo el programa no se a caído, como puede ver espera el tiempo que le indicamos, realiza la conexión y si algo sale mal (cosa que se indica con una excepción) el bloque catch correspondiente nos lo informa que ocurrió algo y previene que se detenga el ciclo.

No solo eso, sino que como es en el run donde se efectúa todo el proceso si inicia de nuevo el servicio MySQL

Tras reiniciar MySQL
Tras reiniciar MySQL

El programa vuelve a funcionar sin problema.

Obviamente para sacarle ventaja a todo esto debe diseñar su programa para que una excepción de ese tipo no vaya a causar errores de calculo o duplicidad de datos, la solución mas adecuada dependerá del caso especifico que este manejando.


Espero que esta entrada les fuera de utilidad y si fue así y desean cooperar con la causa.

ko-fi

Anuncios

Ejecutar periódicamente una función en Java

Función periodica
Función periódica

Una de las situaciones con las que seguro tendrá que lidiar al realizar una aplicación es ejecutar un método en forma periódica cada vez que pase un intervalo de tiempo especificado, ya sea para verificar el estado de un sensor, consultar o actualizar una base de datos, generar un reporte o enviar datos a un servidor, pero no se preocupe, hacer esto en Java es muy sencillo gracias a las clases Timer y TimerTask.

Timer

La clase Timer nos permite ejecutar una función en forma periódica a un intervalo especificado, su uso es bastante sencillo basta con crear un objeto Timer y usar el método scheduleAtFixedRate el cual toma tres argumentos que son los siguientes:

  • task, un objeto TimerTask cuyo método run se ejecutara al intervalo indicado
  • delay, la cantidad de milisegundos que queremos esperar antes de comenzar
  • period, cada cuanto en milisegundo queremos ejecutar el método run del objeto TimerTask

Esto en código se ve de la siguiente manera:

temporizador.scheduleAtFixedRate(tarea, 0, 1000*segundos);

Como notara no es nada del otro mundo, basta con pasarle los parámetros indicados y una vez pasado el retraso indicado por delay la función se llamara cada tantos milisegundos hasta que termine el programa o cancele la ejecución del objeto Timer llamando al método cancel.

TimerTask

Ya definimos cuando y cada cuanto queremos que una función se ejecute, ahora llego el momento de definir dicha función, para hacer esto debemos crear una subclase de TimerTask y redefinir el método run de modo que ejecute el código que nosotros queramos, como se ve en el siguiente ejemplo

package mx.com.hash.tareaprogramada;

import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class Tarea extends TimerTask {
    static private final Logger LOGGER = Logger.getLogger("mx.com.hash.tareaprogramada.Tarea");
    private Integer contador;    

    public Tarea() {
        contador = 0;
    }

    @Override
    public void run() {
        LOGGER.log(Level.INFO, "Numero de ejecución {0}", contador);
        contador++;
    }

}

De nuevo dentro del método run puede poner el código que quiera, llamar a otras clases y demas, no sienta que debe limitarse a funciones de la subclase de TimerTask.

Otro detalle a recordar es que el Timer llama al método run del objeto que le pasamos, de modo que si almacena información en ese objeto esta estará disponible entre cada ejecución del Timer, esto quedara mas claro en el ejemplo.

Ejemplo

Para dejar mas en claro todo esto hagamos un pequeño ejemplo, llamando a una función cada 5 segundos que nos escriba en pantalla cuantas veces hemos llamado a la función, para esto usaremos el siguiente código.

package mx.com.hash.tareaprogramada;

import java.util.Timer;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class TareaProgramada {
    static private final Logger LOGGER = Logger.getLogger("mx.com.hash.tareaprogramada.TareaProgramada");

    static public void main(String[] args){
        Tarea tarea = new Tarea();
        Timer temporizador = new Timer();
        Integer segundos = 5;

        temporizador.scheduleAtFixedRate(tarea, 0, 1000*segundos);
    }
}
package mx.com.hash.tareaprogramada;

import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class Tarea extends TimerTask {
    static private final Logger LOGGER = Logger.getLogger("mx.com.hash.tareaprogramada.Tarea");
    private Integer contador;    

    public Tarea() {
        contador = 0;
    }

    @Override
    public void run() {
        LOGGER.log(Level.INFO, "Numero de ejecución {0}", contador);
        contador++;
    }

}

Y al ejecutarlo veremos la siguiente salida.

Ejemplo de función periodica
Ejemplo de función periódica

Como puede ver el objeto tarea no se destruye durante la ejecución del Timer, por lo que la información en el persiste e ejecución en ejecución.

¿Que pasa si la función tarda mucho en ejecutarse?

Un caso que puede presentarse es que la función tarde tanto en ejecutarse que llegue el momento de volverla a ejecutar y aun no halla acabado, cosa muy posible si depende de conexiones a base de datos, servidores externos o conexiones, ¿En ese caso que pasaría?

Bueno hagamos la prueba, modifiqué la clase Tarea para que quede así

package mx.com.hash.tareaprogramada;

import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class Tarea extends TimerTask {
    static private final Logger LOGGER = Logger.getLogger("mx.com.hash.tareaprogramada.Tarea");
    private Integer contador;    

    public Tarea() {
        contador = 0;
    }

    @Override
    public void run() {
        LOGGER.log(Level.INFO, "Numero de ejecución {0}", contador);
        contador++;

        try {
            // Con esto hacemos que la funcion tarde *mas* en ejecutarse que
            // el periodo especificado
            Thread.sleep(10000);
        } catch (InterruptedException ex) {
            LOGGER.log(Level.SEVERE, "Error de interrupcion");
        }
    }

}<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

Lo que hacemos aquí es agregar un retraso  a la función run de modo que tarde 10 segundos en ejecutarse pero no modificamos lo demas, de modo que el temporizador ejecutara cada 5 segundos una función que tarda 10 segundos en ejecutarse, el resultado se ve a continuación.

Una función que tarda mas en ejecutarse
Una función que tarda mas en ejecutarse

Como ve el retraso entre llamadas es el indicado por la función, el Timer no llamara al método run sino hasta que este halla acabado por lo que no debe preocuparse de que queden cosas incompletas, lo que si puede pasar es que si ya paso el periodo entre llamadas el método run se llame inmediatamente después de terminar la llamada que tardo demas.

El código de este ejemplo lo puede encontrar aquí: https://gitlab.com/ticomWebcomic/tareaprogramada


Espero que esta entrada fuera de utilidad y si lo fue y quiere cooperar con la causa tengo una pagina en Ko-fi para aceptar donativos

ko-fi

Convertir objetos Java a JSON y de regreso

JSON
JSON

El formato JSON se a convertido rápidamente en un estándar en como enviar y recibir información a y desde un webservice, si bien es un formato que se originó en JavaScript usarlo en Java es muy sencillo gracias a la librería Gson.

Gson

Gson es una librería en Java creada por Google que permite convertir un objeto Java a JSON y un JSON a un objeto Java, su uso es increíblemente sencillo, por eso se lo recomiendo.

Convirtiendo un Objeto Java a JSON.

Como primer ejemplo veamos como se convierte un objeto Java a JSON, para esto necesitara crear un objeto de clase Gson el cual se encargara de realizar la conversión y además deberá de asegurarse que la clase del objeto que desea convertir tenga todos los getter y setter necesarios, como se ve a continuación.

package mx.com.pydee.jsonexample;

import java.math.BigDecimal;

/**
 *
 * @author David
 */
public class DetallesVenta {
    private String producto;
    private BigDecimal importe;
    private BigDecimal precioUnitario;
    private Integer cantidad;
    
    public DetallesVenta(String prod, BigDecimal pu, BigDecimal imp, Integer cant) {
        producto = prod;
        precioUnitario = pu;
        importe = imp;
        cantidad = cant;
    }

    /**
     * @return the producto
     */
    public String getProducto() {
        return producto;
    }

    /**
     * @param producto the producto to set
     */
    public void setProducto(String producto) {
        this.producto = producto;
    }

    /**
     * @return the importe
     */
    public BigDecimal getImporte() {
        return importe;
    }

    /**
     * @param importe the importe to set
     */
    public void setImporte(BigDecimal importe) {
        this.importe = importe;
    }

    /**
     * @return the precioUnitario
     */
    public BigDecimal getPrecioUnitario() {
        return precioUnitario;
    }

    /**
     * @param precioUnitario the precioUnitario to set
     */
    public void setPrecioUnitario(BigDecimal precioUnitario) {
        this.precioUnitario = precioUnitario;
    }

    /**
     * @return the cantidad
     */
    public Integer getCantidad() {
        return cantidad;
    }

    /**
     * @param cantidad the cantidad to set
     */
    public void setCantidad(Integer cantidad) {
        this.cantidad = cantidad;
    }
}

Esto es necesario ya que de otro modo le objeto Gson mandara errores de acceso a las propiedades del objeto.

Ya que halla cumplido esos dos detalles la conversión es tan sencilla como llamar al método toJson del objeto Gson, como se ve a continuación.

String JSON = gson.toJson(detalle1);

Y el resultado se vera mas o menos asi

{"producto":"Aceite","importe":120.0,"precioUnitario":12.00,"cantidad":10}

Si el objeto que convirtió a JSON contenia un arreglo de objetos estos se agregaran con la notación adecuada, como se ve a continuación

{
	"fecha": "Jul 10, 2018 1:12:58 AM",
	"cliente": "CLIENTE DE PRUEBA",
	"detalle": [{
		"producto": "Aceite",
		"importe": 120.0,
		"precioUnitario": 12.00,
		"cantidad": 10
	}, {
		"producto": "Anticongelante",
		"importe": 90.0,
		"precioUnitario": 45.00,
		"cantidad": 2
	}, {
		"producto": "Reparacion Rin",
		"importe": 400.0,
		"precioUnitario": 400.00,
		"cantidad": 1
	}]
}

Convirtiendo de JSON a Objetos Java

Al usar un webservice no solo enviaremos información en forma de JSON sino que también la recibiremos en el mismo forma, asi que veamos como convertir de JSON a un objeto Java, la misma observación aplica aquí, el objeto receptor debe de tener todos los getter y setters necesarios o tendrá un error de conversión

El método usado para realizar la conversión es fromJson el cual toma dos argumentos, la cadena String con el JSON y la clase del objeto al que queremos convertir el Json.

Con la cadena de texto debe recordar un detalle importante y es que las comillas dobles son caracteres reservados en Java por lo que recomiendo revisar el Json que regreso el webservice y remplazar esas por comillas simples, para evitar problemas de conversión.

Con respecto a la clase del objeto esa se obtiene facilmente con la propiedad .class de la clase que deseamos usar

Para simplificar las cosas a continuación se presenta un ejemplo

String jsonComplejo = "{'fecha':'Jul 9, 2018 3:37:49 PM','cliente':'LIMPIEZA Y ASEO PROFESIONAL','detalle':[{'producto':'Cloro','importe':150.0,'precioUnitario':15.00,'cantidad':10},{'producto':'Escoba','importe':30,'precioUnitario':30.00,'cantidad':1},{'producto':'Aromatizante ambiental','importe':100.0,'precioUnitario':10.00,'cantidad':10}]}";

Venta venta2 = gson.fromJson(jsonComplejo, Venta.class);

Como notara la conversión puede encargarse sin problemas de un arreglo contenido

El ejemplo completo con lo puede encontrar aquí: https://gitlab.com/ticomWebcomic/JsonExample

Referencias.

Pagina oficial de Gson: https://github.com/google/gson


Espero que esta entrada fuera de utilidad y si lo fue y quiere cooperar con la causa tengo una pagina en Ko-fi para aceptar donativos

ko-fi

BigDecimal, no mas errores de redondeo

Suma de Doubles
Aquí veremos como evitar esto

Si ha usado Java para el desarrollo de aplicaciones donde los datos que maneje representen dinero seguramente se a encontrado con situaciones donde las operaciones con decimales le dan resultados incorrectos, específicamente en los decimales con el resultado quedando centésimas arriba o abajo del resultado correcto.

Esto se debe a la forma en que funciona la aritmética binaria a la hora de representar los decimales, si buen para la mayoría de los casos esos errores son demasiado pequeños las operaciones financieras usualmente implican la combinación ganadora de exigir precisión y operaciones sobre una cantidad significativa de datos de modo que esos pequeños errores de la aritmética binaria se acumulan y termina con una factura un centavo entero mas grande de lo esperado, y si cree que no le van a pelear ese centavo se equivoca, cuando es dinero hasta una décima de centavo basta para darse un encontrón con un cliente.

Por fortuna ya desde hace varias versiones Java incluye una clase que permite hacer esas operaciones sin preocuparnos por errores causados por la aritmética binaria, la clase BigDecimal.

BigDecimal.

La clase BigDecimal nos permite representar un numero decimal con las siguientes características

  • Operaciones decimales: Las operaciones sobre este tipo se realizan internamente en base 10, eso implica una perdida de rendimiento pero a cambio los errores de aritmética binaria se previenen por completo.
  • Precisión arbitraria: No se impone un limite en la cantidad de decimales, la cantidad de decimales a usar esta limitada únicamente por la memoria del sistema
  • Inmutable: Una vez creado un objeto BigDecimal el valor de este NO puede ser modificado, cada operación le generara un objeto nuevo con el valor resultado de la operación.

Si bien la perdida de rendimiento puede sonar preocupante esta esta en el area de mili/micro segundos, el usuario de su aplicación financiera no va a notar la diferencia e incluso si lo nota ese milisegundo es preferible a pelearse con un cliente por un centavo.

Operaciones con BigDecimal.

Las operaciones disponibles son las que espera, suma, resta, multiplicación, división, modulo, potencia, etcetera.

Sin embargo al momento de usarlas debe de tomar dos consideraciones, primero ya que en Java la sobrecarga de operadores no existe debera de usar los métodos .add, .multiply, .divide, .substract, .pow, etc

BigDecimal valor = new BigDecimal("10.0");
BigDecimal factor = new BigDecimal("5.5");

System.out.println("Suma: " + valor.add(factor));
System.out.println("Resta: " + valor.subtract(factor));
System.out.println("Multiplicación: " + valor.multiply(factor));
// MathContext indica cuandos bits deseamos usar para decimales, esto es debe especificar
// para manejar los casos donde la cantidad de decimales sea enorme (1/3 por ejemplo)
System.out.println("Division: " + valor.divide(factor, MathContext.DECIMAL128));
System.out.println("Potencia: " + valor.pow(3));

Hay que notar que las operaciones donde sea posible que ocurran numeros con decimales infinitos se debe especificar la cantidad de decimales a usar via la constante MathContext adecuada.

Otro detalle es que los objetos BigData son inmutables, por lo que las operaciones crean un objeto nuevo para el resultado por lo que debe asignar ese resultado a un objeto BigDecimal para poder usarlo

BigDecimal resultado = valor.add(factor);
System.out.println(“El resultado se guardo y accede desde la variable resultado: ” + resultado);

Ejemplo

Y para no dejar esta entrada sin ejemplo veamos como seria la sumatoria de la imagen en el inicio de la entrada usando BigDecimal en lugar de Double

package mx.hashCode.bigDecimal;

import java.math.BigDecimal;

public class App {    
    static public void main(String[] args){
        BigDecimal resultado = new BigDecimal("0.0");
        BigDecimal cent = new BigDecimal("0.01");

        resultado = resultado.add(cent);
        resultado = resultado.add(cent);
        resultado = resultado.add(cent);
        resultado = resultado.add(cent);
        resultado = resultado.add(cent);
        resultado = resultado.add(cent);

        System.out.println("Resultado de sumar 6 veces el valor 0.01 guardando en una variable BigDecimal");
        System.out.println(resultado);
    }
}

Y el resultado seria el siguiente:

Resultado de sumar 6 veces el valor 0.01 guardando en una variable BigDecimal
0.06

Referencias:

Por que usar BigDecimal (y no Double) para calculos aritmeticos financieros


Espero que esta entrada fuera de utilidad y si lo fue y quiere cooperar con la causa tengo una pagina en Ko-fi para aceptar donativos

ko-fi

Gracias y nos vemos en la próxima entrada :).

Cambiar la altura de las celdas de una Tabla en iText 7

Altura de celdas
Altura de celdas

Cuando pone una tabla en su documento iText 7 puede llegar a pensar que seria bueno aumentar o reducir la altura de las filas en una tabla ya sea para ahorrar espacio al imprimir o mejorar la legibilidad del documento, aquí esta como.

Método .setHeight(altura)

No hay una forma de cambiar la altura de todas las celdas de una tabla de golpe por lo que si queremos especificar la altura lo tenemos que hacer celda por celda, esto es crear un objeto Cell, agregar el contenido, indicar la altura que deseamos con .setHeight(altura) y agrega esa celda a la tabla, un ejemplo de como se ve a continuación.

// Creamos una celda
Cell celda1 = new Cell();

// Agregamos el contenido de la celda
celda1.add(new Paragraph("Celda 1"));

// indicamos la altura para la celda
celda1.setHeight(altura);

// agregamos esa celda a la tabla
tabla.addCell(celda1);

Hay una observación que hacer, aunque uno podria pensar que se podria crear una tabla donde cada celda tenga una altura diferente este no es el caso, en iText la celda mas alta de una fila es la que determinara la altura de toda esa fila por lo que debe tomar eso en consideración al diseñar su tabla.

Ejemplo

Ahora pasemos a un ejemplo, via un método crearemos una tabla y especificaremos la altura de las tablas, llamaremos ese método varias veces para generar varias tablas con celdas de diferente altura y las agregaremos al documento, el resultado sera como en la figura al inicio, una serie de tablas con celdas de altura incremental.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package mx.hash.fontsize.tablecellheight;

import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import java.io.FileNotFoundException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class CellHeight {

    private static Logger LOGGER = Logger.getLogger("mx.hash.fontsize.tablecellheight.CellHeight");

    public Table crearTablaConAtura(float altura) {
        // Creamos la tabla
        float[] anchos = {150f, 150f, 150f};
        Table tabla = new Table(anchos);
        
        // Creamos las celdas
        Cell celda1 = new Cell();
        Cell celda2 = new Cell();
        Cell celda3 = new Cell();

        celda1.add(new Paragraph("Celda 1"));
        // Indicamos la altura para la celda 1
        celda1.setHeight(altura);

        celda2.add(new Paragraph("Celda 2"));
        // Indicamos la altura para la celda 2
        celda2.setHeight(altura);

        celda3.add(new Paragraph("Celda 3"));
        // Indicamos la altura para la celda 3
        celda3.setHeight(altura);
        
        // Agregamos las celdas a la tabla
        tabla.addCell(celda1);
        tabla.addCell(celda2);
        tabla.addCell(celda3);

        return tabla;
    }

    static public void main(String[] args) {
        PdfWriter pdfWriter;
        try {
            // Creamos un documento pdf con iText
            pdfWriter = new PdfWriter("./fontSize.pdf");
            PdfDocument pdfDoc = new PdfDocument(pdfWriter);
            Document doc = new Document(pdfDoc, PageSize.LETTER);
            
            CellHeight generadorTablas = new CellHeight();
            
            doc.add( new Paragraph("Altura 12f, demasiado chico para el tamaño de texto, por lo que no se vera, tenga cuidado con esto y redusca el tamaño de fuente acordemente") );
            doc.add( generadorTablas.crearTablaConAtura(12f) );
            
            Table tablaChica = generadorTablas.crearTablaConAtura(12f);
            // Cambiamos el tamaño de texto
            tablaChica.setFontSize(8f);
            
            doc.add( new Paragraph("Con un tamaño de texto acorde ya se ve mejor") );
            doc.add(tablaChica);
            
            doc.add( new Paragraph("Altura 22f") );
            doc.add( generadorTablas.crearTablaConAtura(22f) );
            
            doc.add( new Paragraph("Altura 32f") );
            doc.add( generadorTablas.crearTablaConAtura(32f) );
            
            doc.add( new Paragraph("Altura 42f") );
            doc.add( generadorTablas.crearTablaConAtura(42f) );
            
            doc.add( new Paragraph("Altura 52f") );
            doc.add( generadorTablas.crearTablaConAtura(52f) );
            
            doc.add( new Paragraph("El tamaño se respeta por *fila*, al cambiar de fila usa la altura de la celda mas alta de esa fila") );
            
            Table tablaExtendida =  generadorTablas.crearTablaConAtura(52f);
            tablaExtendida.addCell( new Paragraph("Esta celda deberia ser de tamaño normal!") );
            
            Cell celdaGrande = new Cell();
            
            celdaGrande.setHeight(100f);
            celdaGrande.add( new Paragraph("Esta celda cambia el tamaño de toda la fila"));
            tablaExtendida.addCell(celdaGrande);
            
            doc.add(tablaExtendida);

            doc.close();

        } catch (FileNotFoundException ex) {
            LOGGER.log(Level.SEVERE, null, ex);
        }
    }

}

Hay dos casos extra que se presentan en el ejemplo.

El primero es que pasa si indica un altura demasiado pequeña para que el texto aparesca completamente, en ese caso el texto simplemente no aparecera, esto no genera ningun mensaje de error por lo que debe de verificarlo con cuidado al reducir la altura de las celdas, una correción simple es reducir el tamaño del texto como se vio en una entrada anterior.

Lo segundo y mas importante es lo que mencionamos previamente, la altura de toda la fila es controlada por la celda mas alta de la fila, independientemente de lo que indiquen las demas tablas y ese tamaño se resetea cada cambio de fila, por lo que si se quiere poner creativo con eso debe tomar muy en cuenta cuantas celdas van en cada fila.

El código completo de este ejemplo puede ser encontrado en https://gitlab.com/ticomWebcomic/TableCellHeight

Espero que esta entrada les fuera de utilidad y desean cooperar, me pueden invitar una cerveza: https://www.paypal.me/hashRaygoza/20mxn Gracias y nos vemos en la próxima :).

 

Cambiar el tamaño de fuente en iText 7

Diferentes tamaños de fuente en iText7
Diferentes tamaños de fuente en iText7

Si esta utilizando iText 7 para generar reportes o documentos que vayan a ser impresos hay que ser considerados con el tamaño de texto, puede ser que se quiera ahorrar papel y que todo el reporte quepa en una sola pagina o que recuerde que en papel no hay función zoom así que debe ser legible, estas situaciones suelen implicar cambiar el tamaño del texto, ya sea reducirlo o aumentarlo y no se ustedes pero la documentación oficial de iText 7 es un poco críptica al respecto (o al menos Google tiene problemas en hallar donde estaba) asi que veamos como aumentar y reducir el tamaño del texto en iText 7 (Si se pregunta por que estoy repitiendo tanto el numero de versión es por que lo que lea de iText 5 NO funciona en iText 7).

El Método .setFontSize

Por fortuna esta vez iText 7 va a estar muy a nuestra favor ya que con la mayoría de los elementos que utilizan texto, como Paragraph y Table, basta con llamar el método

.setFontSize(tamaño)

donde tamaño es un valor float que indica el tamaño de la fuente que deseamos usar y con eso el tamaño cambia.

Ejemplo

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package mx.hash.fontsize.fontsizeitext;

import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import java.io.FileNotFoundException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author david
 */
public class FontSizeExample {
    private static Logger LOGGER = Logger.getLogger("mx.hash.fontsize.fontsizeitext.FontSizeExample");
    
    static public void main(String[] args) {
        try {
            // Creamos un documento pdf con iText
            PdfWriter pdfWriter = new PdfWriter("./fontSize.pdf");
            PdfDocument pdfDoc = new PdfDocument(pdfWriter);
            Document doc = new Document(pdfDoc, PageSize.LETTER);
            
            // Creamos unos parrafos
            Paragraph parrafo1 = new Paragraph("Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default Prueba con el tamaño por default");
            Paragraph parrafo2 = new Paragraph("Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña Demostracion de letra mas pequeña");
            Paragraph parrafo3 = new Paragraph("Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande Ejemplo con una letra mas grande ");
            
            // Creamos unas tablas
            float[] anchos = {150f, 150f, 150f};
            Table tabla1 = new Table(anchos);
            Table tabla2 = new Table(anchos);
            Table tabla3 = new Table(anchos);
            
            // Agregamos contenido a las tablas
            tabla1.addCell("Ejemplo con");
            tabla1.addCell("Con tamaño");
            tabla1.addCell("default");
            
            tabla2.addCell("Ejemplo con");
            tabla2.addCell("Con tamaño");
            tabla2.addCell("mas pequeño");
            
            tabla3.addCell("Ejemplo con");
            tabla3.addCell("Con tamaño");
            tabla3.addCell("mas grande");            
            
            // Cambiamos el tamaño de fuente del parrafo 2 lo hacemos mas pequeño
            parrafo2.setFontSize(8f);
            
            // Cambiamos el tamaño de fuente del parrafo 3 lo hacemos mas grande
            parrafo3.setFontSize(20f);
            
            // Cambiamos el tamaño de fuente de la tabla 1, lo hacemos mas pequeño
            tabla2.setFontSize(8f);
            
            // Cambiamos el tamaño de fiente de la tabla 2, lo hacemos mas grande
            tabla3.setFontSize(20f);            
            
            doc.add(parrafo1);
            doc.add(parrafo2);
            doc.add(parrafo3);
            doc.add(tabla1);
            doc.add(tabla2);
            doc.add(tabla3);
            
            doc.close();
        } catch (FileNotFoundException ex) {
            LOGGER.log(Level.SEVERE, null, ex);
        }
    }
    
}

Como puede ver cambiar el tamaño es tan sencillo como llamar un solo método, notara que al poner el tamaño puse una f después de los números esto es para que el compilador de Java sepa que es un numero de tipo float.

El código completo del proyecto puede localizarlo en https://gitlab.com/ticomWebcomic/FontSizeiText


Espero que esta entrada les fuera de utilidad y desean cooperar, me pueden invitar una cerveza: https://www.paypal.me/hashRaygoza/20mxn Gracias y nos vemos en la próxima :).

 

Imprimiendo en Java SIN mostrar el diálogo de impresión

Impreso automáticamente y sin pedir permiso
Impreso automáticamente y sin pedir permiso

Si se decida a programar sistemas hechos a la medida para diferentes negocios el generar reportes no debiera ser algo inusual, mucho menos que el cliente pida que se puedan mandar a la impresora pero ¿Que tal que el cliente pida que un reporte se imprima a una hora especifica?, esto suele tener un detalle inesperado, el que se imprima incluso si no hay nadie en la oficina para presionar el botón Imprimir, ¿Suena complicado?, ¿Algo que solo se puede hacer vía un cable serial y mandando los bits directamente?, nada de eso, aquí le digo como.

Para esto nos basaremos en lo visto en una entrada previa, por lo que le recomiendo le eche un vistazo si no lo ha hecho ya.

Mandando un documento iText directo a la impresora

Ya que halla refrescado su memoria veamos como mandar algo a la impresora sin mostrar el dialogo de impresión o pedir permiso al usuario (Obviamente solo usara este poder para el bien, ¿verdad?).

Teoría

De las entradas anteriores con la librería PdfBox seguro recordara el la clase PrinterJob es la que se encarga de mandar el documento a la impresora y para indicar la impresora necesitamos llamar al método printDialog, este nos deja seleccionar la impresora y nos pide la confirmación, pero esta no es la única forma de indicar la impresora.

Esto lo hacemos con la clase PrintService esta nos deja indicar directamente a PrinterJob la impresora saltandonos el dialogo de selección de impresora y la confirmación.

Ahora ¿Como creamos ese objeto PrintService? bueno para eso necesitamos dos cosas, primero saber el nombre exacto de la impresora que desea usar, hay dos formas de hacer esto, una es ver el dialogo de impresoras del sistema operativo, como se ve en la figura.

Así se ve en Fedora Linux, puede variar según su sistema operativo
Así se ve en Fedora Linux, puede variar según su sistema operativo

La otra es ejecutar la siguiente función la cual le listara todas las impresoras disponibles en su sistema.

/**
* Muestra en pantalla la lista de todas las impresoras disponibles en el
* sistema
*/
public void listarImpresoras() {
	PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
	System.out.println("Lista de impresoras disponibles");

	for (PrintService printService: printServices) {
		System.out.println("\t" + printService.getName());
	}
}

Esta función le presentara en pantalla un listado completo de las impresoras del sistema, recuerde correrla en un proyecto que incluya PdfBox, la salida sera similar a la figura.

Como ve solo tengo una impresora conectada.
Como ve solo tengo una impresora conectada.

Como puede ver en el código de la función de búsqueda, la lista completa de impresoras nos la proporciono el método PrintServiceLookup.lookupPrintServices(null, null); el cual listo todas las impresoras, bueno usando esa misma idea podemos crear un método que nos regrese la impresora con el nombre indicado que seria como se ve a continuación

/**
* Nos regresa el PrintService que representa la impresora con el nombre que
* le indiquemos
* @param printerName nombre de la impresora que deseamos usar
* @return PrintService que representa la impresora que deseamos usar
*/
private PrintService findPrintService(String printerName) {
	PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
	for (PrintService printService: printServices) {
		System.out.println(printService.getName());

		if (printService.getName().trim().equals(printerName)) {
			return printService;
		}
	}
	return null;
}

Basta con pasarle de parámetro el nombre de la impresora que deseamos usar y nos regresara el PrintService adecuado.

Lo ultimo que necesitaría hacer ya que tenga el PrintService es indicarle al objeto PrinterJob que deseamos usar ese PrintService, esto se logra con una sola linea de código que es:

printerJob.setPrintService(myPrintService);

Hecho esto basta con llamar el método print() y el documento se enviara directo a la impresora.

Pero para hacer mas claro todo veamos un ejemplo.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package mx.hash.impresioninmediata;

import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.swing.JOptionPane;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.printing.PDFPageable;

/**
 *
 * @author david
 */
public class ImpresionInmediata {

    private final static Logger LOGGER = Logger.getLogger("mx.hash.impresioninmediata.ImpresionInmediata");

    static public void main(String[] args) {
        ImpresionInmediata printer = new ImpresionInmediata();

        printer.listarImpresoras();

        try {
            ByteArrayOutputStream documentoBytes = printer.crearDocumentoiText();
            printer.imprimir(documentoBytes);
        } catch (IOException | PrinterException ex) {
            JOptionPane.showMessageDialog(null, "Error de impresion", "Error", JOptionPane.ERROR_MESSAGE);
            LOGGER.log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Envia a imprimir el ByteArrayOutoutStream creado de un documento iText
     *
     * @param documentoBytes
     * @throws IOException
     * @throws PrinterException
     */
    public void imprimir(ByteArrayOutputStream documentoBytes) throws IOException, PrinterException {

        // Aqui convertimos la el arreglo de salida a uno de entrada que podemos
        // mandar a la impresora
        ByteArrayInputStream bais = new ByteArrayInputStream(documentoBytes.toByteArray());

        // Creamos un PDDocument con el arreglo de entrada que creamos        
        PDDocument document = PDDocument.load(bais);

        PrintService myPrintService = this.findPrintService("Deskjet-1510-series");
        PrinterJob printerJob = PrinterJob.getPrinterJob();

        printerJob.setPageable(new PDFPageable(document));
        printerJob.setPrintService(myPrintService);

        printerJob.print();

    }

    /**
     * Muestra en pantalla la lista de todas las impresoras disponibles en el
     * sistema
     */
    public void listarImpresoras() {
        PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
        System.out.println("Lista de impresoras disponibles");

        for (PrintService printService : printServices) {
            System.out.println("\t" + printService.getName());
        }
    }

    /**
     * Nos regresa el PrintService que representa la impresora con el nombre que
     * le indiquemos
     * @param printerName nombre de la impresora que deseamos usar
     * @return PrintService que representa la impresora que deseamos usar
     */
    private PrintService findPrintService(String printerName) {
        PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
        for (PrintService printService : printServices) {
            System.out.println(printService.getName());

            if (printService.getName().trim().equals(printerName)) {
                return printService;
            }
        }
        return null;
    }

    /**
     * Crea un documento via la libreria iText y lo almacena como un
     * ByteArrayOutputStream
     *
     * @return Documento iText en formato ByteArrayOutputStream
     */
    public ByteArrayOutputStream crearDocumentoiText() {
        // Es en este ByteArrayOutputStream donde se pone el documento una vez 
        // que se llama a documento.close()
        ByteArrayOutputStream documentoBytes = new ByteArrayOutputStream();

        PdfWriter pdfWriter = new PdfWriter(documentoBytes);
        PdfDocument pdfDoc = new PdfDocument(pdfWriter);

        Document documento = new Document(pdfDoc, PageSize.LETTER);
        documento.add(new Paragraph("Inicia el reporte"));
        documento.add(this.crearTabla());

        documento.close();

        return documentoBytes;
    }

    private Table crearTabla() {
        float[] anchos = {50F, 50F, 50F};
        Table tablaEncabezado = new Table(anchos);

        tablaEncabezado.setWidth(500F);

        tablaEncabezado.addCell("Hora Inicio");
        tablaEncabezado.addCell("Hora Fin");
        tablaEncabezado.addCell("");
        tablaEncabezado.addCell("Fecha Inicio");
        tablaEncabezado.addCell("Fecha Fin");
        tablaEncabezado.addCell("Fin de Turno");

        return tablaEncabezado;
    }    

}

Al correr este ejemplo muy posiblemente la indicara un error (a menos que por casualidad su impresora se llama exactamente igual que la mia), no se preocupe al correr el programa la lista completa de impresoras aparecerá en pantalla, solamente vea cual impresora desea usar y acomode la siguiente linea

PrintService myPrintService = this.findPrintService("Deskjet-1510-series");

Poniendo el nombre de la impresora como argumento a esa función, hecho esto el documento se imprimirá la siguiente vez que corra el programa.

El ejemplo completo lo puede encontrar aquí: https://github.com/HashRaygoza/ImpresionInmediata


Espero que esta entrada les fuera de utilidad y desean cooperar, me pueden invitar una cerveza: https://www.paypal.me/hashRaygoza/20mxn Gracias y nos vemos en la próxima :).