Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Docx4j - How to replace placeholder with value

Tags:

java

docx

docx4j

I've been trying to work through the examples FieldMailMerge and VariableReplace but can't seem to get a local test case running. I'm basically trying to start with one docx template document and have it create x docx documents from that one template with the variables replaced.

In the code below docx4jReplaceSimpleTest() tries to replace a single variable but fails to do so. The ${} values in the template files are removed as part of the processing therefore I believe it's finding them but not replacing them for some reason. I understand it could be due to formatting as explained in the comments of the sample code but for troubleshooting just to get something working I'm trying it anyways.

In the code below docx4jReplaceTwoPeopleTest(), the one I want to get working, I'm trying to do it in what I believe is the proper way, but that's not finding or replacing anything. It's not even removing the ${} from the docx file.

public static void main(String[] args) throws Exception
{
    docx4jReplaceTwoPeopleTest();
    docx4jReplaceSimpleTest();
}

private static void docx4jReplaceTwoPeopleTest() throws Exception
{
      String docxFile = "C:/temp/template.docx";

      WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(new java.io.File(docxFile));

      List<Map<DataFieldName, String>> data = new ArrayList<Map<DataFieldName, String>>();

      Map<DataFieldName, String> map1 = new HashMap<DataFieldName, String>();
      map1.put(new DataFieldName("Person.Firstname"), "myFirstname");
      map1.put(new DataFieldName("Person.Lastname"), "myLastname");
      data.add(map1);

      Map<DataFieldName, String> map2 = new HashMap<DataFieldName, String>();
      map2.put(new DataFieldName("Person.Firstname"), "myFriendsFirstname");
      map2.put(new DataFieldName("Person.Lastname"), "myFriendsLastname");
      data.add(map2);

      org.docx4j.model.fields.merge.MailMerger.setMERGEFIELDInOutput(OutputField.KEEP_MERGEFIELD);

      int x=0;
      for(Map<DataFieldName, String> docMapping: data) 
      {
        org.docx4j.model.fields.merge.MailMerger.performMerge(wordMLPackage, docMapping, true);
        wordMLPackage.save(new java.io.File("C:/temp/OUT__MAIL_MERGE_" + x++ + ".docx") );
      }
}

private static void docx4jReplaceSimpleTest() throws Exception
{
      String docxFile = "C:/temp/template.docx";

      WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(new java.io.File(docxFile));

      HashMap<String, String> mappings = new HashMap<String, String>();
      mappings.put("Person.Firstname", "myFirstname");
      mappings.put("Person.Lastname", "myLastname");

      MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
      documentPart.variableReplace(mappings);

    wordMLPackage.save(new java.io.File("C:/temp/OUT_SIMPLE.docx") );
}

The docx file consists of the following text (no formatting is done):

This is a letter to someone
Hi ${Person.Firstname} ${Person.Lastname},
How are you?
Thank you again. I wish to see you soon ${Person.Firstname}
Regards,
Someone

Notice that I'm also trying to replace Person.Firstname at least twice as well. As the lastname is not even replaced I don't think this has anything to do with it but I'm adding it just in case.

like image 621
Stephane Grenier Avatar asked Dec 10 '13 02:12

Stephane Grenier


2 Answers

I had the same issue and of course I could not force user to do some extra stuff when composing their word document so I decided to just write an algo to scan the whole document for expressions appending run after run, inserting replacement value and remove expressions in the second run. In case other people may need it below is what I did. I got the class from somewhere so it may be familiar. I just added the method searchAndReplace()

package com.my.docx4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;

import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.ContentAccessor;
import org.docx4j.wml.Text;

public class Docx4j {

    public static void main(String[] args) throws Docx4JException, IOException, JAXBException {
        String filePath = "C:\\Users\\markamm\\Documents\\tmp\\";
        String file = "Hello.docx";

        Docx4j docx4j = new Docx4j();
        WordprocessingMLPackage template = docx4j.getTemplate(filePath+file);

//      MainDocumentPart documentPart = template.getMainDocumentPart();

        List<Object> texts = getAllElementFromObject(
                template.getMainDocumentPart(), Text.class);
        searchAndReplace(texts, new HashMap<String, String>(){
            {
                this.put("${abcd_efg.soanother_hello_broken_shit}", "Company Name here...");
                this.put("${I_dont_know}", "Hmmm lemme see");
                this.put("${${damn.right_lol}", "Gotcha!!!");
                this.put("${one_here_and}", "Firstname");
                this.put("${one}", "ChildA");
                this.put("${two}", "ChildB");
                this.put("${three}", "ChildC");
            }
            @Override
            public String get(Object key) {
                // TODO Auto-generated method stub
                return super.get(key);
            }
        });

        docx4j.writeDocxToStream(template, filePath+"Hello2.docx");
    }

    public static void searchAndReplace(List<Object> texts, Map<String, String> values){

        // -- scan all expressions  
        // Will later contain all the expressions used though not used at the moment
        List<String> els = new ArrayList<String>(); 

        StringBuilder sb = new StringBuilder();
        int PASS = 0;
        int PREPARE = 1;
        int READ = 2;
        int mode = PASS;

        // to nullify
        List<int[]> toNullify = new ArrayList<int[]>();
        int[] currentNullifyProps = new int[4];

        // Do scan of els and immediately insert value
        for(int i = 0; i<texts.size(); i++){
            Object text = texts.get(i);
            Text textElement = (Text) text;
            String newVal = "";
            String v = textElement.getValue();
//          System.out.println("text: "+v);
            StringBuilder textSofar = new StringBuilder();
            int extra = 0;
            char[] vchars = v.toCharArray();
            for(int col = 0; col<vchars.length; col++){
                char c = vchars[col];
                textSofar.append(c);
                switch(c){
                case '$': {
                    mode=PREPARE;
                    sb.append(c);
//                  extra = 0;
                } break;
                case '{': {
                    if(mode==PREPARE){
                        sb.append(c);
                        mode=READ;
                        currentNullifyProps[0]=i;
                        currentNullifyProps[1]=col+extra-1;
                        System.out.println("extra-- "+extra);
                    } else {
                        if(mode==READ){
                            // consecutive opening curl found. just read it
                            // but supposedly throw error
                            sb = new StringBuilder();
                            mode=PASS;
                        }
                    }
                } break;
                case '}': {
                    if(mode==READ){
                        mode=PASS;
                        sb.append(c);
                        els.add(sb.toString());
                        newVal +=textSofar.toString()
                                +(null==values.get(sb.toString())?sb.toString():values.get(sb.toString()));
                        textSofar = new StringBuilder();
                        currentNullifyProps[2]=i;
                        currentNullifyProps[3]=col+extra;
                        toNullify.add(currentNullifyProps);
                        currentNullifyProps = new int[4];
                        extra += sb.toString().length();
                        sb = new StringBuilder();
                    } else if(mode==PREPARE){
                        mode = PASS;
                        sb = new StringBuilder();
                    }
                }
                default: {
                    if(mode==READ) sb.append(c);
                    else if(mode==PREPARE){
                        mode=PASS;
                        sb = new StringBuilder();
                    }
                }
                }
            }
            newVal +=textSofar.toString();
            textElement.setValue(newVal);
        }

        // remove original expressions
        if(toNullify.size()>0)
        for(int i = 0; i<texts.size(); i++){
            if(toNullify.size()==0) break;
            currentNullifyProps = toNullify.get(0);
            Object text = texts.get(i);
            Text textElement = (Text) text;
            String v = textElement.getValue();
            StringBuilder nvalSB = new StringBuilder();
            char[] textChars = v.toCharArray();
            for(int j = 0; j<textChars.length; j++){
                char c = textChars[j];
                if(null==currentNullifyProps) {
                    nvalSB.append(c);
                    continue;
                }
                // I know 100000 is too much!!! And so what???
                int floor = currentNullifyProps[0]*100000+currentNullifyProps[1];
                int ceil = currentNullifyProps[2]*100000+currentNullifyProps[3];
                int head = i*100000+j;
                if(!(head>=floor && head<=ceil)){
                    nvalSB.append(c);
                } 

                if(j>currentNullifyProps[3] && i>=currentNullifyProps[2]){
                    toNullify.remove(0);
                    if(toNullify.size()==0) {
                        currentNullifyProps = null;
                        continue;
                    }
                    currentNullifyProps = toNullify.get(0);
                }
            }
            textElement.setValue(nvalSB.toString());
        }
    }

    private WordprocessingMLPackage getTemplate(String name)
            throws Docx4JException, FileNotFoundException {
        WordprocessingMLPackage template = WordprocessingMLPackage
                .load(new FileInputStream(new File(name)));
        return template;
    }

    private static List<Object> getAllElementFromObject(Object obj,
            Class<?> toSearch) {
        List<Object> result = new ArrayList<Object>();
        if (obj instanceof JAXBElement)
            obj = ((JAXBElement<?>) obj).getValue();

        if (obj.getClass().equals(toSearch))
            result.add(obj);
        else if (obj instanceof ContentAccessor) {
            List<?> children = ((ContentAccessor) obj).getContent();
            for (Object child : children) {
                result.addAll(getAllElementFromObject(child, toSearch));
            }

        }
        return result;
    }

    private void replacePlaceholder(WordprocessingMLPackage template,
            String name, String placeholder) {
        List<Object> texts = getAllElementFromObject(
                template.getMainDocumentPart(), Text.class);

        for (Object text : texts) {
            Text textElement = (Text) text;
            if (textElement.getValue().equals(placeholder)) {
                textElement.setValue(name);
            }
        }
    }

    private void writeDocxToStream(WordprocessingMLPackage template,
            String target) throws IOException, Docx4JException {
        File f = new File(target);
        template.save(f);
    }
}
like image 197
demotics2002 Avatar answered Oct 06 '22 00:10

demotics2002


The issue is that I was trying to create the placeholders as just plain text within the docx file. What I should've been doing instead is using the MergeField functionality within Word which I didn't fully understand and appreciate, hence the confusion. Basically I didn't know that this is what was being meant within the documentation because I'd never used it, I just assumed it was still some kind of xml text replacement.

That being said it's still fairly difficult to find a good explanation of this Word feature. After looking at a few dozen explanations I still couldn't find a nice clean explanation of this Word feature. The best explanation I was able find can be found here. Basically you want to do Step 3.

That being said, once I created MergeFields in Word and ran the code, it worked perfectly. The method to use is docx4jReplaceTwoPeopleTest. The problem wasn't in the code but in my understanding of how it worked within Word.

like image 44
Stephane Grenier Avatar answered Oct 05 '22 23:10

Stephane Grenier