Enhancing Word Document Generation with Java Actions
Introduction
The ability to generate word documents is a feature that often comes up with customers that are evaluating the Mendix platform as a viable solution. The platform does have this feature natively but sometimes is not flexible enough for every customer. One of the drawbacks is not being able to make any changes to a template during runtime. Although there are alternatives in the Mendix Appstore (linked below), this functionality can easily be built using java actions.
Goal
Today I’m going to explore building functionality that allows a user to upload a word document that contains token placeholders and then perform find and replace functionality with the Apache POI Library to generate a word document with data from a Mendix entity.
Apache POI
The first step is to figure out how to accomplish this with the Apache POI library. After looking at the documentation and doing some quick googling, I was able to find a handful solutions. The one I particularly chose to go with was one that I found on stack overflow. At the time when I did this proof of concept, POI version 4.0.1 was the latest version, so I will continue this blog post with that version. Version 4.1 has since been released and will most likely work with the same code, but some time will need to be spent figuring out what versions of the POI dependencies to use.
Mendix Project
Now that we did our research on the POI library, it would be a good time to create a blank Mendix project. Once that is done, lets setup the domain model to be able to upload a word document that we can perform some testing on.
I created an entity called “WordDoc” and set the generalization to System.FileDocument.
Then on the home page I added a datagrid for this entity and then used the “Generate Page” functionality to create a New/Edit page. This will be used for testing the initial java action to ensure it works before we build token replacing functionality.
Java Action
The next step would be to start on our java action that will perform the find and replace functionality. We can start with replacing some static text in a word document, and then move onto making this action dynamic. The first step is to make a microflow with the WordDoc as a parameter and then create a java action.
Right now, this java action should have two parameters, one to pass in a System.FileDocument, and then another to store the new contents of the word document. Pass your WordDoc object to this java action and then create a new WordDoc object to pass as the output (for your new wordDoc, don’t forget to set the name with the .docx extension).
Now we need to deploy our application for eclipse so we can open it up in an IDE and create our java action. This can be done either at the top with Project > Deploy for eclipse or by pressing F6.
Once that is finished up you can open your project in eclipse. If you’ve never done that before, here is a link the documentation on how to do so. If you prefer not to use eclipse (I prefer Intellij), then open your project in any IDE and locate your java action. The java action will be located in the javasource folder > myfirstmodule > actions.
Once we open that file up, delete the line of code between the first “BEGIN USER CODE” and “END USER CODE”. Then we can check out our stack overflow post and grab the code that was given.
The first thing we need to do is create an XWPFDocument from our WordDoc object. That can be done with this line of code:
XWPFDocument doc = new XWPFDocument(Core.getFileDocumentContent(getContext(), Document.getMendixObject()));
Then we can use the rest of the code from the stack overflow post to perform the find and replace. This example will replace the word “needle” with “haystack”.
for (XWPFParagraph p : doc.getParagraphs()) { List<XWPFRun> runs = p.getRuns(); if (runs != null) { for (XWPFRun r : runs) { String text = r.getText(0); if (text != null && text.contains("needle")) { text = text.replace("needle", "haystack"); r.setText(text, 0); } } } } for (XWPFTable tbl : doc.getTables()) { for (XWPFTableRow row : tbl.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { for (XWPFParagraph p : cell.getParagraphs()) { for (XWPFRun r : p.getRuns()) { String text = r.getText(0); if (text != null && text.contains("needle")) { text = text.replace("needle", "haystack"); r.setText(text,0); } } } } } }
Then once we perform that find and replace, we need to convert the XWPF document back into a Mendix File Document and save it. This can be done with this code:
ByteArrayOutputStream output = new ByteArrayOutputStream(); doc.write(output); InputStream input = new ByteArrayInputStream(output.toByteArray()); Core.storeFileDocumentContent(getContext(), Output.getMendixObject(), input);
Then our java action is set to return a Boolean so you can return true at the end of this code.
Now that all of that code is in place, you might notice that your IDE is showing errors. We need to add the POI library and its dependencies into our userlib folder, and then write the imports for our java action.
I was able to grab the POI jar file from their website, but you can also grab it from my example project linked at the bottom of this app. Along with the POI library, you need to grab the jar files for its dependencies. Here is the full list of jar files that you need for this action to work:
1. poi-4.0.1
2. poi-ooxml-4.0.1
3. poi-ooxml-schemas-4.0.1
4. sax2r2
5. xbean
6. commons-collections4-4.3
7. commons-compress-1.18
Once these jar files are in your userlib folder, you can add the imports to the top of your java action. If you are using Intellij, “Allt + Enter” will automatically add each one for you.
Here are all my imports:
import com.mendix.core.Core; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.webui.CustomJavaAction; import com.mendix.systemwideinterfaces.core.IMendixObject; import org.apache.poi.xwpf.usermodel.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.List;
Testing
Now that this is done, we can test the java action to see if it works. Before we continue, I would add a download activity to the end of your microflow so you can see your document after it is generated. Then add the microflow that you just created to your datagrid on the homepage and, then run your project locally.
Once that’s done compiling create a .docx document with the word(s) “needle” in it, and then upload it to your application. Select your document and trigger your test microflow. Your new document should download and you can see how the word “needle” was replaced with “haystack”.
Tokens
Now that we have the find and replace functionality built out, we can tweak our java action to use a list of tokens to find and replace the text in a document. Before we do that, let’s create a structure to setup tokens for our template. I was able to get everything to work using the Model Refleciton module, so let’s import that into our project.
The next thing we need to do is add two associations to our WordDoc entity. We want to associate WordDoc to MxObjectType and to Token. MxObjectType is a many to one association, and Token is a many to many association.
Then edit your WordDoc_NewEdit page with these two new references. I added a reference selector for MxObjectType and a reference set selector for Token.
For the edit button in the reference set selector, we are going to use the model reflection page “token_NewEdit”. The new button I used a microflow to perform some validation and then showed the same page. Here is an example new button microflow:
Once that is all setup, we need to make the changes to our java action to accept the list of tokens, and to make the find and replace dynamic. The first thing we need to do is add a couple more parameters to our java action and to make a few changes to our test microflow.
For the java action, add two more parameters. One for the list of tokens, and one for a context object (the object that you are getting your data from).
The Context object is a type parameter, meaning that we can pass any object type to it.
Then add a new entity to your domain model to create an object that contains data. I’m going to create a non-persistent entity called “test” with one attribute that has a default value to test with.
Then in your microflow create the test object and retrieve the list of tokens to pass to the java action.
Now the final piece is to make some tweaks to your java action. You want to wrap all of your existing code in a for loop using the token list, and create two new variables called “findtext” and “replaceText”. The find text will be the token’s combined name, and the replace text will be a variable that uses the model reflection token replacer function to get the value from the context object.
String findText = token.getCombinedToken(); String replaceText = mxmodelreflection.TokenReplacer.replaceToken(getContext(), findText, token.getMendixObject(), ContextObj);
See image at bottom of completed java action
Side note: Don’t forget to setup the model reflection module and run it after you compile.
Test 2
Now you can recompile your project and setup a new test. Create a new Word document with a token on it. I uploaded a word doc with the token “” on it. Then I created the token on my the WordDoc_NewEdit page.
When I ran my test microflow again a new word document was downloaded with the sentence “This is a test to see if the token will work”.
Conclusion
If you need simple find and replace functionality then this solution may be ideal if you are willing to support it during upgrades and fixing any conflicts with any other libraries. If you need something more complex then I would check out some of the paid alternatives in the Appstore.
Word Document Generation Alternatives
· Document Generation 4 Mendix
· Word Document Generation for Mendix
Check out my example project on github:
https://github.com/austinmcnicholas/Java-Action-Blog-Example
Full java action code // This file was generated by Mendix Modeler. // // WARNING: Only the following code will be retained when actions are regenerated: // - the import list // - the code between BEGIN USER CODE and END USER CODE // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. // Special characters, e.g., é, ö, à, etc. are supported in comments. package poi.actions; import com.mendix.core.Core; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.webui.CustomJavaAction; import mxmodelreflection.proxies.Token; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.xwpf.usermodel.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.util.List; import com.mendix.systemwideinterfaces.core.IMendixObject; public class ReplaceWordTokens_POI extends CustomJavaAction<java.lang.Boolean> { private IMendixObject __WordTemplate; private system.proxies.FileDocument WordTemplate; private IMendixObject __Output; private system.proxies.FileDocument Output; private java.util.List<IMendixObject> __TokenList; private java.util.List<mxmodelreflection.proxies.Token> TokenList; private IMendixObject ContextObj; public ReplaceWordTokens_POI(IContext context, IMendixObject WordTemplate, IMendixObject Output, java.util.List<IMendixObject> TokenList, IMendixObject ContextObj) { super(context); this.__WordTemplate = WordTemplate; this.__Output = Output; this.__TokenList = TokenList; this.ContextObj = ContextObj; } @java.lang.Override public java.lang.Boolean executeAction() throws Exception { this.WordTemplate = __WordTemplate == null ? null : system.proxies.FileDocument.initialize(getContext(), __WordTemplate); this.Output = __Output == null ? null : system.proxies.FileDocument.initialize(getContext(), __Output); this.TokenList = new java.util.ArrayList<mxmodelreflection.proxies.Token>(); if (__TokenList != null) for (IMendixObject __TokenListElement : __TokenList) this.TokenList.add(mxmodelreflection.proxies.Token.initialize(getContext(), __TokenListElement)); // BEGIN USER CODE /* * if uploaded doc then use HWPF else if uploaded Docx file use * XWPFDocument */ XWPFDocument doc = new XWPFDocument(Core.getFileDocumentContent(getContext(), WordTemplate.getMendixObject())); for (Token token : TokenList) { String findText = token.getCombinedToken(); String replaceText = mxmodelreflection.TokenReplacer.replaceToken(getContext(), findText, token.getMendixObject(), ContextObj); for (XWPFParagraph p : doc.getParagraphs()) { List<XWPFRun> runs = p.getRuns(); if (runs != null) { for (XWPFRun r : runs) { String text = r.getText(0); if (text != null && text.contains(findText)) { text = text.replace(findText, replaceText);//your content r.setText(text, 0); } } } } // handle if there is a table in the document. for (XWPFTable tbl : doc.getTables()) { for (XWPFTableRow row : tbl.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { for (XWPFParagraph p : cell.getParagraphs()) { for (XWPFRun r : p.getRuns()) { String text = r.getText(0); if (text != null && text.contains(findText)) { text = text.replace(findText, replaceText); r.setText(text, 0); } } } } } } } // store in a Mendix file document ByteArrayOutputStream output = new ByteArrayOutputStream(); doc.write(output); InputStream input = new ByteArrayInputStream(output.toByteArray()); Core.storeFileDocumentContent(getContext(), Output.getMendixObject(), input); return true; // END USER CODE } /** * Returns a string representation of this action */ @java.lang.Override public java.lang.String toString() { return "ReplaceWordTokens_POI"; } // BEGIN EXTRA CODE // END EXTRA CODE }