Hace poco tuve que manejar contratos que se firmarían por N número de usuarios, por lo que tuve que buscar una forma de tener un solo documento el cuál sería mi plantilla y de ahí generar el PDF personalizado para cada usuario.

Para este post usaremos Maven para el manejo de dependencias. Las librerías necesarias para poder realizar esto son Apache POI y xdocreport de Opensagres.

Las tareas las separé en dos clases, una para el manejo del archivo Word y la otra para la generación del PDF.

Lo primero que debemos hacer es incluir las librerías en el archivo pom.xml, como se muestra a continuación:

<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi</artifactId>
	<version>4.1.2</version>
</dependency>
<dependency>
	<groupId>	org.apache.poi</groupId>
	<artifactId>poi-ooxml</artifactId>
	<version>4.1.2</version>
</dependency>
<dependency>
	<groupId>fr.opensagres.xdocreport</groupId>
	<artifactId>fr.opensagres.poi.xwpf.converter.pdf</artifactId>
	<version>2.0.2</version>
</dependency>

Ahora debemos actualizar nuestro proyecto haciendo clic derecho en él, en la opción de maven y update project.

Ya que nuestro proyecto se ha actualizado y tiene las dependencias instaladas, procedemos a modificar nuestras clases Java.

Comenzaremos con la clase DocxHandling la cual se encargará de abrir el documento Word que no llegue y comenzará a sustituir los textos párrafo por párrafo.

public InputStream replaceTexts() throws Exception {
	
	//Validamos que hayamos recibido el path del documento original 
	if(originalFilePath == null) throw new NullPointerException("Debe proporcionar el path del archivo original");
	
	//Validamos que hayamos recibido los textos a reemplazar 
	if(textsToReplace == null || textsToReplace.isEmpty()) throw new Exception("Debe proporcionar los textos a reemplazar");
	
	//Generamos un nombre para nuestro archivo temporal
	temporalFileName = UUID.randomUUID().toString();

	//Abrimos nuestro documento
	doc = new XWPFDocument(new FileInputStream(originalFilePath));

	//comenzamos con la iteracion de los textos a reemplazar
	Iterator it = textsToReplace.keySet().iterator();
	while(it.hasNext()) {
		String item = (String) it.next();
		//Enviamos a reemplazar el texto y guardamos sustituimos el documento en la misma variable
		doc = replaceText(doc, item, textsToReplace.get(item));
	}
	
	//Una vez se haya terminado de reemplazar, guardamos el documento en un archivo temporal
	//Para ello general el archivo temporal que pasaremos a nuestro metodo que se encarga de realizar la escritura
	String tmpDestinationPath = Files.createTempFile(temporalFileName, "." + Constantes.EXTENSION_DOCX).toAbsolutePath().toString();
	
	//Guardamos el documento en el archivo
	saveWord(tmpDestinationPath, 
			doc);
	
	//Retornamos un ImputStream por si el usuario va a trabajar con el
	return new FileInputStream(tmpDestinationPath);
}

A continuación, muestro el método que se encarga de recorrer el archivo y sustituir los textos por el valor recibido:

private static XWPFDocument replaceText(XWPFDocument doc, String findText, String replaceText){
		//Realizamos el recorrido de todos los parrafos
        for (int i = 0; i < doc.getParagraphs().size(); ++i ) { 
			//Asignamos el parrafo a una variable para trabajar con ella 
            XWPFParagraph s = doc.getParagraphs().get(i); 
            
			//De ese parrafo recorremos todos los Runs
            for (int x = 0; x < s.getRuns().size(); x++) { 
				//Asignamos el run en turno a una varibale
                XWPFRun run = s.getRuns().get(x); 
				//Obtenemos el texto 
				String text = run.text();
				//Validamos si el texto contiene el key a sustituir 
				if(text.contains(findText)) {
					//Si lo contiene lo reemplazamos y guardamos en una variable
					String replaced = run.getText(run.getTextPosition()).replace(findText, replaceText);
					//Pasamos el texto nuevo al run
					run.setText(replaced, 0);
                }
            }
        } 
		//Retornamos el documento con los textos ya reemplazados
        return doc;
    }

Por último, les voy a mostrar el método que se encarga de escribir el documento en el archivo temporal que hemos creado:

private static void saveWord(String filePath, XWPFDocument doc) throws FileNotFoundException, IOException{
	FileOutputStream out = null;
	try{
		out = new FileOutputStream(filePath);
		doc.write(out);
	}
	finally{
		out.close();
	}
}

El método anterior no lo comenté ya que es muy sencillo.

Ahora la clase DocxToPdfConverter, que se encarga de convertir nuestro archivo Word (.docx) a PDF . Esta clase es muy sencilla, ya que solamente se debe abrir el documento y se le pasa a la librería para que realice la conversión, a continuación, les muestro el método:

public InputStream convert(InputStream in) {
	try {
		//Generamos nuestro archivo temporal donde se guardara el PDF
		File tmpFile = Files.createTempFile(UUID.randomUUID().toString(), "." + Constantes.EXTENSION_PDF).toFile();
		//Abrimos el documento word que recibimos a traves del InputStream
		XWPFDocument document = new XWPFDocument(in);
		//Generamos la instancia de las opciones del PDF, en este caso la de default
		PdfOptions options = PdfOptions.create();
		//Generamos nuestro OputStream que se encargará de hacer la escritura en el archivo
		OutputStream out = new FileOutputStream(tmpFile);
		//Paramos nuestro documento, el output y las opciones a la librería 
		PdfConverter.getInstance().convert(document, out, options);
		//En este punto el archivo temporal ya contiene nuestro PDF y lo retornamos como un InputStream
		return new FileInputStream(tmpFile);
	} catch (IOException ex) {
		System.out.println(ex.getMessage());
	}
	return null;
}

En los métodos de ambas clases yo trabajo con Streams ya que tengo otro servicio que se encarga de persistir los archivos, pero también podemos trabajar con rutas (paths) para indicar donde queremos que se guarden los archivos generados.

Aquí dejo el enlace al repositorio con los archivos mencionados.