2012-02-21 244 views
8

Es necesario abrir una (yo no soy exigente, sólo quiero que funcione) .doc/.dot/.docx/.dotx documento, analizarlo para marcadores de posición (o algo similar), poner mis propios datos, y luego de retorno generada .doc/.docx/.pdf documento.¿Cómo abrir y manipular documento/plantilla de Word en Java?

Y además de todo eso, necesito las herramientas para lograr que sea gratis.

He buscado algo que huela mis necesidades, pero no puedo encontrar nada. Herramientas como Docmosis, Javadocx, Aspose, etc. son comerciales. Por lo que he leído Apache POI no está cerca de implementar esto con éxito (actualmente no tienen ningún desarrollador oficial que trabaje en Word como parte del framework).

Lo único que parece que podría hacer el truco es la API de OpenOffice UNO. Pero ese es un byte bastante grande para alguien que nunca ha usado esta API (como yo).

Así que si voy a entrar en esto, necesito asegurarme de estar en el camino correcto.

¿Puede alguien darme algún consejo al respecto?

+0

¿Si solo se trata de reemplazar algunos marcadores de posición, ¿por qué Java? – Alfabravo

+1

¿Has probado docx4j? – JasonPlutext

+0

Java porque es solo una pequeña parte de un proyecto realmente grande. Iré con docx4j. – kensvebary

Respuesta

23

Sé que ha pasado mucho tiempo desde que publiqué esta pregunta, y dije que publicaría mi solución cuando terminara. Así que aquí está.

Espero que ayude a alguien algún día. Esta es una clase trabajadora completa, y todo lo que tiene que hacer es colocarla en su aplicación y colocar el directorio TEMPLATE_DIRECTORY_ROOT con las plantillas .docx en su directorio raíz.

El uso es muy simple. Coloca marcadores de posición (clave) en su archivo .docx y luego pasa el nombre de archivo y el Mapa que contiene los pares clave-valor correspondientes para ese archivo.

¡Disfrútalo!

import java.io.BufferedInputStream; 
import java.io.BufferedOutputStream; 
import java.io.BufferedReader; 
import java.io.Closeable; 
import java.io.File; 
import java.io.FileInputStream; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.InputStreamReader; 
import java.io.OutputStream; 
import java.net.URI; 
import java.util.Deque; 
import java.util.Enumeration; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.LinkedList; 
import java.util.Map; 
import java.util.UUID; 
import java.util.zip.ZipEntry; 
import java.util.zip.ZipFile; 
import java.util.zip.ZipOutputStream; 

import javax.faces.context.ExternalContext; 
import javax.faces.context.FacesContext; 
import javax.servlet.http.HttpServletResponse; 

public class DocxManipulator { 

    private static final String MAIN_DOCUMENT_PATH = "word/document.xml"; 
    private static final String TEMPLATE_DIRECTORY_ROOT = "TEMPLATES_DIRECTORY/"; 


    /* PUBLIC METHODS */ 

    /** 
    * Generates .docx document from given template and the substitution data 
    * 
    * @param templateName 
    *   Template data 
    * @param substitutionData 
    *   Hash map with the set of key-value pairs that represent 
    *   substitution data 
    * @return 
    */ 
    public static Boolean generateAndSendDocx(String templateName, Map<String,String> substitutionData) { 

     String templateLocation = TEMPLATE_DIRECTORY_ROOT + templateName; 

     String userTempDir = UUID.randomUUID().toString(); 
     userTempDir = TEMPLATE_DIRECTORY_ROOT + userTempDir + "/"; 

     try { 

      // Unzip .docx file 
      unzip(new File(templateLocation), new File(userTempDir));  

      // Change data 
      changeData(new File(userTempDir + MAIN_DOCUMENT_PATH), substitutionData); 

      // Rezip .docx file 
      zip(new File(userTempDir), new File(userTempDir + templateName)); 

      // Send HTTP response 
      sendDOCXResponse(new File(userTempDir + templateName), templateName); 

      // Clean temp data 
      deleteTempData(new File(userTempDir)); 
     } 
     catch (IOException ioe) { 
      System.out.println(ioe.getMessage()); 
      return false; 
     } 

     return true; 
    } 


    /* PRIVATE METHODS */ 

    /** 
    * Unzipps specified ZIP file to specified directory 
    * 
    * @param zipfile 
    *   Source ZIP file 
    * @param directory 
    *   Destination directory 
    * @throws IOException 
    */ 
    private static void unzip(File zipfile, File directory) throws IOException { 

     ZipFile zfile = new ZipFile(zipfile); 
     Enumeration<? extends ZipEntry> entries = zfile.entries(); 

     while (entries.hasMoreElements()) { 
      ZipEntry entry = entries.nextElement(); 
      File file = new File(directory, entry.getName()); 
      if (entry.isDirectory()) { 
      file.mkdirs(); 
      } 
      else { 
      file.getParentFile().mkdirs(); 
      InputStream in = zfile.getInputStream(entry); 
      try { 
       copy(in, file); 
      } 
      finally { 
       in.close(); 
      } 
      } 
     } 
     } 


    /** 
    * Substitutes keys found in target file with corresponding data 
    * 
    * @param targetFile 
    *   Target file 
    * @param substitutionData 
    *   Map of key-value pairs of data 
    * @throws IOException 
    */ 
    @SuppressWarnings({ "unchecked", "rawtypes" }) 
    private static void changeData(File targetFile, Map<String,String> substitutionData) throws IOException{ 

     BufferedReader br = null; 
     String docxTemplate = ""; 
     try { 
      br = new BufferedReader(new InputStreamReader(new FileInputStream(targetFile), "UTF-8")); 
      String temp; 
      while((temp = br.readLine()) != null) 
       docxTemplate = docxTemplate + temp; 
      br.close(); 
      targetFile.delete(); 
     } 
     catch (IOException e) { 
      br.close(); 
      throw e; 
     } 

     Iterator substitutionDataIterator = substitutionData.entrySet().iterator(); 
     while(substitutionDataIterator.hasNext()){ 
      Map.Entry<String,String> pair = (Map.Entry<String,String>)substitutionDataIterator.next(); 
      if(docxTemplate.contains(pair.getKey())){ 
       if(pair.getValue() != null) 
        docxTemplate = docxTemplate.replace(pair.getKey(), pair.getValue()); 
       else 
        docxTemplate = docxTemplate.replace(pair.getKey(), "NEDOSTAJE"); 
      } 
     } 

     FileOutputStream fos = null; 
     try{ 
      fos = new FileOutputStream(targetFile); 
      fos.write(docxTemplate.getBytes("UTF-8")); 
      fos.close(); 
     } 
     catch (IOException e) { 
      fos.close(); 
      throw e; 
     } 
    } 

    /** 
    * Zipps specified directory and all its subdirectories 
    * 
    * @param directory 
    *   Specified directory 
    * @param zipfile 
    *   Output ZIP file name 
    * @throws IOException 
    */ 
    private static void zip(File directory, File zipfile) throws IOException { 

     URI base = directory.toURI(); 
     Deque<File> queue = new LinkedList<File>(); 
     queue.push(directory); 
     OutputStream out = new FileOutputStream(zipfile); 
     Closeable res = out; 

     try { 
      ZipOutputStream zout = new ZipOutputStream(out); 
      res = zout; 
      while (!queue.isEmpty()) { 
      directory = queue.pop(); 
      for (File kid : directory.listFiles()) { 
       String name = base.relativize(kid.toURI()).getPath(); 
       if (kid.isDirectory()) { 
       queue.push(kid); 
       name = name.endsWith("/") ? name : name + "/"; 
       zout.putNextEntry(new ZipEntry(name)); 
       } 
       else { 
       if(kid.getName().contains(".docx")) 
        continue; 
       zout.putNextEntry(new ZipEntry(name)); 
       copy(kid, zout); 
       zout.closeEntry(); 
       } 
      } 
      } 
     } 
     finally { 
      res.close(); 
     } 
     } 

    /** 
    * Sends HTTP Response containing .docx file to Client 
    * 
    * @param generatedFile 
    *   Path to generated .docx file 
    * @param fileName 
    *   File name of generated file that is being presented to user 
    * @throws IOException 
    */ 
    private static void sendDOCXResponse(File generatedFile, String fileName) throws IOException { 

     FacesContext facesContext = FacesContext.getCurrentInstance(); 
     ExternalContext externalContext = facesContext.getExternalContext(); 
     HttpServletResponse response = (HttpServletResponse) externalContext 
       .getResponse(); 

     BufferedInputStream input = null; 
     BufferedOutputStream output = null; 

     response.reset(); 
     response.setHeader("Content-Type", "application/msword"); 
     response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); 
     response.setHeader("Content-Length",String.valueOf(generatedFile.length())); 

     input = new BufferedInputStream(new FileInputStream(generatedFile), 10240); 
     output = new BufferedOutputStream(response.getOutputStream(), 10240); 

     byte[] buffer = new byte[10240]; 
     for (int length; (length = input.read(buffer)) > 0;) { 
      output.write(buffer, 0, length); 
     } 

     output.flush(); 
     input.close(); 
     output.close(); 

     // Inform JSF not to proceed with rest of life cycle 
     facesContext.responseComplete(); 
    } 


    /** 
    * Deletes directory and all its subdirectories 
    * 
    * @param file 
    *   Specified directory 
    * @throws IOException 
    */ 
    public static void deleteTempData(File file) throws IOException { 

     if (file.isDirectory()) { 

      // directory is empty, then delete it 
      if (file.list().length == 0) 
       file.delete(); 
      else { 
       // list all the directory contents 
       String files[] = file.list(); 

       for (String temp : files) { 
        // construct the file structure 
        File fileDelete = new File(file, temp); 
        // recursive delete 
        deleteTempData(fileDelete); 
       } 

       // check the directory again, if empty then delete it 
       if (file.list().length == 0) 
        file.delete(); 
      } 
     } else { 
      // if file, then delete it 
      file.delete(); 
     } 
    } 

    private static void copy(InputStream in, OutputStream out) throws IOException { 

     byte[] buffer = new byte[1024]; 
     while (true) { 
      int readCount = in.read(buffer); 
      if (readCount < 0) { 
      break; 
      } 
      out.write(buffer, 0, readCount); 
     } 
     } 

     private static void copy(File file, OutputStream out) throws IOException { 
     InputStream in = new FileInputStream(file); 
     try { 
      copy(in, out); 
     } finally { 
      in.close(); 
     } 
     } 

     private static void copy(InputStream in, File file) throws IOException { 
     OutputStream out = new FileOutputStream(file); 
     try { 
      copy(in, out); 
     } finally { 
      out.close(); 
     } 
    } 

} 
+1

Gracias, @kensvebary. Lamentablemente, este enfoque no funcionará de manera confiable. Word inyecta todo tipo de etiquetas de adición en el medio de las palabras, lo que dificulta encontrar los marcadores de posición. Supongamos que tiene un marcador de posición llamado %% employeeId %%. Word puede (o no puede) hacer que como tal: '%% EMPLOYEEID% ' Lo mismo ocurriría con RTF. – PeterToTheThird

+0

Lo probé y funciona muy bien. Para aprobar, reemplace el contenido que puede usar [[ReplaceContent]]. Esto funciona bien –

+0

Es un buen comienzo, aunque puede no ser muy robusto.Incluso cuando usa marcadores de posición simples, me pregunto qué sucede al final de línea: supongo que Word está codificando devoluciones de carro, guiones ... con etiquetas xml que pueden romper la cadena de reemplazo. – xtof54

0

He estado más o menos en la misma situación que usted, tuve que modificar un montón de plantillas de combinación de MS Word a la vez. Después de haber buscado mucho en Google para tratar de encontrar una solución Java, finalmente instalé Visual Studio 2010 Express, que es gratis e hice el trabajo en C#.

+0

¡Gracias por la ayuda chicos! Me diste algunas pautas. Creo que comenzaré con la biblioteca docx4j. Y agrega un código personalizado donde sea necesario. Voy a publicar la solución cuando termine. – kensvebary

+0

@kensvebary ¿cómo está su solución al final? Necesito la misma solución que la tuya, por favor dame tu opinión como ves. ¡Gracias de antemano! – malajisi

4

Desde un archivo docx no es más que un archivo ZIP de archivos XML (además de los archivos binarios para objetos incrustados como imágenes), nos encontramos con ese requisito por descomprimir el archivo zip, que alimenta el document.xml a un motor de plantillas (utilizamos freemarker) que hace la fusión para nosotros, y luego comprime el documento de salida para obtener el nuevo archivo docx.

El documento de la plantilla es simplemente un docx normal con expresiones/directivas freemarker integradas, y se puede editar en Word.

Dado que (un) zipping se puede hacer con el JDK, y Freemarker es de código abierto, no se le cobra ninguna tarifa de licencia, ni siquiera por la palabra misma.

La limitación es que este enfoque solo puede emitir archivos docx o rtf, y el documento de salida tendrá el mismo tipo de archivo que la plantilla. Si necesita convertir el documento a otro formato (como PDF) tendrá que resolver ese problema por separado.

+0

Otra limitación es que no puede hacer nada que cree relaciones entre partes (por ejemplo, imágenes, hipervínculos) – JasonPlutext

+0

Sí, si necesita insertar imágenes dinámicas, deberá desarrollar una etiqueta de marcador libre para insertar la referencia de la imagen e incluir la imagen datos en el zip. Off-hand, no veo el problema con los hipervínculos. – meriton

3

Terminé confiando en Apache Poi 3.12 y párrafos de procesamiento (extrayendo separadamente párrafos también de tablas, encabezados/pies de página y notas a pie de página, ya que dichos párrafos no son devueltos por XWPFDocument.getParagraphs()).

El código de procesamiento (~100 lines) y las pruebas unitarias son here on github.

0

Hace poco resolví un problema similar: "Una herramienta que acepta una plantilla '.docx' archivo, procesa el archivo mediante la evaluación del contexto de parámetros pasados ​​y genera un archivo '.docx' como resultado del proceso. "

finalmente dios nos trajo scriptlet4dox :). las principales características de este producto es: 1. inyección de código maravilloso como secuencias de comandos en el archivo de plantilla (inyección de parámetros, etc.) 2. bucle sobre elementos de la colección en la tabla

y así muchas otras características. pero, como comprobé, el último compromiso del proyecto se realizó hace aproximadamente un año, por lo que hay una probabilidad de que el proyecto no sea compatible con nuevas características y nuevas correcciones de errores. esta es tu elección para usarlo o no.

Cuestiones relacionadas