Java Mail Merge with objects


#1

Hi there,

I'm evaluating the Java version of Aspose.Words. I primarily want to use it to perform a server-side mail merge. The problem I have is that the only documented methods that I can see on the Java version of the MailMerge class are:

execute(java.sql.ResultSet resultSet)

execute(java.lang.String[] fieldNames, java.lang.Object[] values)

My server-side objects are proper Java classes e.g. a java.util.List of my own Customer objects. How can I get the MailMerge object to take my list of Customer objects and iterate over it, calling e.g. customer.getFirstName() for the firstName field in the document to be merged?

I don't want to implement the ResultSet interface to do this so that only leaves the second method. But it's not documented. I know it works if I pass in single string arrays for fields and values e.g.

MailMerge mm = wordDoc.getMailMerge();
String[] fields = new String[]{"Firstname"};
String[] values = new String[]{"Kevin"};
mm.execute(fields, values);

but can it be used with multiple values to somehow get around my problem?

Thanks for any help,

Kevin.


#2

The MailMerge.execute method that takes arrays of strings and objects is designed to work with one record only.

The way to populate the document with data depends on what sort of document you are trying to build. Let me know more about what you are trying to achieve, attach the document to the post here.

In general, for our mail merge engine to work, it needs to retrieve data from something like a data table. Basically it needs to be able to obtain row after row and retrieve field values from those rows by name. What sort of an interface do you think could work? We never had this problem in Aspose.Words for .NET since its mail merge can accept DataTable, DataView and IDataReader which are good vehicles for representing data.


#3

Hi Roman,

Cheers for the reply. I’m trying to build a very simple mail merge but I use the standard DAO (Data Access Objects) pattern in my application and hence encapsulate all database access in DAO classes. This means that if I want to pass a java.sql.ResultSet to the mail merge code, I have to break my application design and I don’t want to have to do that.

In standard Java, if you want to retrieve row after row, you generally have some code pass you something that implements the java.util.Collection interface (e.g. java.util.List, java.util.Set), and then get a java.util.Iterator object and use that to iterate over the collection. I don’t know much about .NET but I’d imagine DataTable, DataView and IDataReader are probably structured along similar lines.

I’ve attached some sample code below (couldn’t see a way to attach, sorry!) that illustrates 2 options for how the MailMerge code could work with some example classes that are not my real-world problem but illustrate the use-case that I have. The only 2 lines that won’t compile are the extra execute() methods I’ve added. Option 1 is a bit horrible because you end up iterating twice - once to build the list of HashMaps and once to do the mail merge. I prefer Option 2.

Let me know if you’ve got any issues with the code or what I’ve said. I appreciate the help - especially considering it must be pretty late in NZ at the mo.!

Kevin.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import com.aspose.words.Document;

public class Example
{

private void exampleCode() throws Exception
{
// get the customer DAO class responsible for all DB access related to
// customers
CustomerDAO custDAO = new CustomerDAO();

// get a list of customers that we want to send a letter to - lets say
// all VIP customers as an example
List customers = custDAO.getVIPCustomers();

// get the word document
Document wordDoc = new Document(“c:\path\to\word.doc”);

// Option 1
// ========
// iterate over the customers and put their properties in a map

Iterator i = customers.iterator();
List customerMappings = new ArrayList();

while (i.hasNext())
{
// create a map of mail merge fields to customer properties
HashMap map = new HashMap();
Customer c = (Customer) i.next();
map.put(“FirstName”, c.getFirstname());
map.put(“Sur_Name”, c.getSurname());

// add the map to a list
customerMappings.add(map);
}

// do the mail merge
// the execute() method here would iterate through the supplied list and
// expect a HashMap at each place in the list. It could then interrogate
// the hashmap for each merge field to see what it’s value (if any) was
wordDoc.getMailMerge().execute(customerMappings);


// Option 2
// ========
// Use Javabean lookups. In JavaBean conventions, if you refer to a property
// as customer.firstname then the getFirstname() method will be called on
// the Customer object. This method tells the mail merge how to map the
// merge fields to the customer object attributes.

// set up a hashmap that records how merge fields are related to customer
// properties
HashMap mergeFields = new HashMap();
mergeFields.put(“FirstName”, “firstname”);
mergeFields.put(“Sur_Name”, “surname”);

// do the mail merge. The code would iterate over the supplied Collection
// (in this case a list) and use the hashmap supplied to map merge fields
// to Javabean properties
wordDoc.getMailMerge().execute(mergeFields, customers);

}

}

/
* DAO class that in the real world would go off to the database to get
* information about customers
*/
class CustomerDAO
{
List getVIPCustomers()
{
Customer c1 = new Customer();
c1.setFirstname(“Bob”);
c1.setFirstname(“Dylan”);

Customer c2 = new Customer();
c2.setFirstname(“Bruce”);
c2.setFirstname(“Springstein”);

ArrayList list = new ArrayList();
list.add(c1);
list.add(c2);

return list;
}
}

/

* Standard Javabean class to represent a customer
*/
class Customer
{
private String m_firstname;
private String m_surname;
public String getFirstname()
{
return m_firstname;
}
public void setFirstname(String firstname)
{
m_firstname = firstname;
}
public String getSurname()
{
return m_surname;
}
public void setSurname(String surname)
{
m_surname = surname;
}



}


#4

Thanks, I now understand the approach with a list of hash maps and another approach with a list of objects with simple getters that are invoked using reflection (not sure what is the word for "reflection" Java). You mention it is a java beans convention to invoke getters using reflection so I hope it is an acceptable practice for many java developers.

We will discuss it with our Java developers and most likely implement one or more MailMerge.execute methods in the next release. It could come out within 2 weeks or so.

By the way, I realized we already have an abstract interface for retrieving data for the mail merge purposes inside our code and that interface is much simpler to implement than ResultSet. Here is the interface, let me know, if we made it public, would it be easy for you to implement this interface to iterate over your classes yourself?

///


/// This interface is to create adapter classes that allow merging
/// from various data sources.
///

abstract class MailMergeDataSource
{
///
/// Returns name of the table this data source represnets.
///

///
abstract String getTableName() throws Exception;

///


/// Advance to the next record in the mail merge data source.
///

abstract boolean moveNext() throws Exception;

///


/// Returns value for the specified name or false if the value is not found.
///

abstract boolean getValue(String name, Object[] value) throws Exception;

///


/// Gets zero based index of the current row being processed.
///

abstract int getCurRow();
}


#5

The reason I wanted to know more about the document is that maybe you can workaround the missing method.

For example, if your document is a kind of letter addressed to a customer and you have many customers to send it to then:

1. Why do you want to merge letters for all customers into one document? Maybe you need to mail merge the document once and save for each customer? The fastest in this case will be to open the document only once and then clone before mail merging.

2. If you still want to mail merge for multiple customers into a single document, you can workaround like this:

open the source document

create a completely empty destination document

for each customer

clone the source document

mail merge one record into the clone

append the document filled with data to the destination document

save the destination document

Essentially, this is what our mail merge does internall for this type of mail merge.


#6

Hi Roman,

I would like to be able to perform what I think of as a ‘normal’ Word Mail Merge. That is, I have a bunch of customers, 1 template document and I perform a merge to generate 1 final document at the end which contains the letters to send to all customers. It is not acceptable to generate a separate document for every customer.

I believe that I am therefore required to pursue option 2 on your post. Below is the code that I believe should work - it is based on the example that I posted earlier. However, I’m uncertain as to how to do a clone and an append. I’m getting an exception when running this code:

Exception in thread “main” java.lang.IllegalArgumentException: The newChild was created from a different document than the one that created this node.
at com.aspose.words.CompositeNode.a(Unknown Source)
at com.aspose.words.CompositeNode.insertAfter(Unknown Source)
at com.aspose.words.CompositeNode.appendChild(Unknown Source)
at com.aspose.words.NodeCollection.add(Unknown Source)
at com.asposetest.ExampleSolutionTest.exampleCode(ExampleSolutionTest.java:67)
at com.asposetest.ExampleSolutionTest.main(ExampleSolutionTest.java:21)

Have you any ideas what I’m doing wrong?

Thanks,

Kevin.

public class ExampleSolutionTest
{

public static void main(String[] args) throws Exception
{
ExampleSolutionTest me = new ExampleSolutionTest();
me.exampleCode();
}

private void exampleCode() throws Exception
{
// get the customer DAO class responsible for all DB access related to
// customers
CustomerDAO custDAO = new CustomerDAO();

// get a list of customers that we want to send a letter to - lets say
// all VIP customers as an example
List customers = custDAO.getVIPCustomers();

// get the source word document
Document wordDoc = new Document(“C:\temp\aspose\aspose.doc”);

// get the target word document
Document targetDoc = new Document();

// set up the fields we’ll bind in for the mail merge
String[] fields = new String[]{“Firstname”};

// get an iterator for the customers
Iterator i = customers.iterator();

// get a list of child nodes for the target doc
NodeCollection nodes = targetDoc.getChildNodes();

while (i.hasNext())
{
// get the customer
Customer c = (Customer) i.next();

// clone the source document
Document clone = wordDoc.deepClone();

// set up the mail merge properties
String[] values = new String[]{c.getFirstname()};

// merge in the fields to this single document
clone.getMailMerge().execute(fields, values);

// append this clone’s contents to the target document
Sections sections = clone.getSections();
for (int j=0; j<sections.getCount(); j++)
{
targetDoc.getSections().add(sections.get(j));
}

}

// save the target doc
targetDoc.save(“C:\temp\aspose\asposeGenerated.doc”);

}

}


#7

To insert contents from one document to another use destination document importNode method for each section of the source document. The resulting section nodes could then be added or inserted to the destination document.


#8

Hi Vladimir,

Thanks - that was exactly what I needed. For anyone who’s interested, my final source is below. If anyone can suggest any optimisations to the source, then I’d be grateful.

Kevin.


public class ExampleSolutionTest
{

public static void main(String[] args) throws Exception
{
ExampleSolutionTest me = new ExampleSolutionTest();
me.exampleCode();
}

private void exampleCode() throws Exception
{
// get the customer DAO class responsible for all DB access related to
// customers
CustomerDAO custDAO = new CustomerDAO();

// get a list of customers that we want to send a letter to - lets say
// all VIP customers as an example
List customers = custDAO.getVIPCustomers();

// get the source word document
Document wordDoc = new Document(“C:\temp\aspose\aspose.doc”);

// get the target word document
Document targetDoc = new Document();

// set up the fields we’ll bind in for the mail merge
String[] fields = new String[]{“Firstname”};

// get an iterator for the customers
Iterator i = customers.iterator();

// get a list of child nodes for the target doc
NodeCollection nodes = targetDoc.getChildNodes();

while (i.hasNext())
{
// get the customer
Customer c = (Customer) i.next();

// clone the source document
Document clone = wordDoc.deepClone();

// set up the mail merge properties
String[] values = new String[]{c.getFirstname()};

// merge in the fields to this single document
clone.getMailMerge().execute(fields, values);

// append this clone’s contents to the target document
Sections sections = clone.getSections();
for (int j=0; j<sections.getCount(); j++)
{
Node n = targetDoc.importNode(sections.get(j), true);
targetDoc.getSections().add(n);
}
}

// save the target doc
targetDoc.save(“C:\temp\aspose\asposeGenerated.doc”);

}

}

/
* DAO class that in the real world would go off to the database to get
* information about customers
*/
class CustomerDAO
{
List getVIPCustomers()
{
Customer c1 = new Customer();
c1.setFirstname(“Bob”);
c1.setSurname(“Dylan”);

Customer c2 = new Customer();
c2.setFirstname(“Bruce”);
c2.setSurname(“Springstein”);

ArrayList list = new ArrayList();
list.add(c1);
list.add(c2);

return list;
}
}
/

* Standard Javabean class to represent a customer
*/
class Customer
{
private String m_firstname;
private String m_surname;
public String getFirstname()
{
return m_firstname;
}
public void setFirstname(String firstname)
{
m_firstname = firstname;
}
public String getSurname()
{
return m_surname;
}
public void setSurname(String surname)
{
m_surname = surname;
}
}


#9

I beleive that cloning of the source document using deepClone is not necessary in your case. Cloning is needed only when you want to have several copies of the same node inside the document.


#10

Vladimir is correct. Clone is not needed if you ImportNode because ImportNode creates a copy of the node with its children. Please have a look at the corresponding methods in the C# API Reference since the Java version is not complete. The product for two platforms is essentially the same piece of code just with different naming conventions. We are going to build the API Reference for Java and .NET from single source documentation soon.

Here is C# code to do what you want. It is included in the demo project with the product installer (in ProductCatalogDemo.cs), you should have the same code in the Java download.

///
/// Copies all sections from one document to another.
///
private void AppendDoc(Document dstDoc, Document srcDoc)
{
for (int i = 0; i < srcDoc.Sections.Count; i++)
{
//First need to import the section into the destination document,
//this translates any document specific lists or styles.
Section section = (Section)dstDoc.ImportNode(srcDoc.SectionsIdea [I], true);
dstDoc.Sections.Add(section);
}
}


#11

Just released Aspose.Words for Java 1.0.2 that provides the IMailMergeDataSource and additional methods in MailMerge to allow mail merge from any custom data source.

All you need to do is to create a class and implement IMailMergeDataSource. It is a simple interface, has only three methods. Get the table name, move to next record/return when EOF is reached, get the field value. Then pass this object to MailMerge.Execute or MailMerge.ExecuteWithRegions.

Here is an example in C# (sorry no example in Java yet), but you will get the idea easily:

///

/// A custom mail merge data source that you implement to allow Aspose.Words

/// to mail merge data from your Customer objects into Microsoft Word documents.

///

public class CustomerMailMergeDataSource : IMailMergeDataSource

{

public CustomerMailMergeDataSource(CustomerList customers)

{

mCustomers = customers;

// When the data source is initialized, it must be positioned before the first record.

mRecordIndex= -1;

}

///

/// The name of the data source. Used by Aspose.Words only when executing mail merge with repeatable regions.

///

public string TableName

{

get { return "Customer"; }

}

///

/// Aspose.Words call this to get a value for every data field.

///

public bool GetValue(string fieldName, out object fieldValue)

{

switch (fieldName)

{

case "FullName":

fieldValue = mCustomers[mRecordIndex].FullName;

return true;

case "Address":

fieldValue = mCustomers[mRecordIndex].Address;

return true;

default:

// A field with this name was not found,

// return false to the Aspose.Words mail merge engine.

fieldValue = null;

return false;

}

}

///

/// A standard implementation for moving to a next record in a collection.

///

public bool MoveNext()

{

if (IsEof)

return false;

mRecordIndex++;

return (!IsEof);

}

private bool IsEof

{

get { return (mRecordIndex >= mCustomers.Count); }

}

private CustomerList mCustomers;

private int mRecordIndex;

}


#12

Hi Roman,

This is basically exactly what I was looking for all along! Great work! There's just 1 small problem....I can't get it to merge in the fields! It just merges in an empty field where my field should be. I think this is probably due to the string array argument to the getValue() method - why is it an array? How does the MailMerge engine underneath deal with it? I'm just putting my string to merge into the array as the first element - is this the right thing to do?

Any help would be appreciated. My source code for the data source is below. All other code is the same as before really.

public class CustomerDataSource implements IMailMergeDataSource
{
List m_customers;
Iterator m_iterator;
Customer m_currentCustomer;

public CustomerDataSource(List customers)
{
m_customers = customers;
m_iterator = customers.iterator();
}

/**
* The name of the data source. Used by Aspose.Words only when executing mail
* merge with repeatable regions.
*/
public String getTableName() throws Exception
{
return "applications";
}

public boolean getValue(String fieldName, Object[] fieldValue) throws Exception
{
String field = null;
if ("Firstname".equals(fieldName))
{
field = m_currentCustomer.getFirstname();
}
else if ("Surname".equals(fieldName))
{
field = m_currentCustomer.getSurname();
}

if (field != null)
{
fieldValue = new String[]{field};
return true;
}
else
{
// field was not found
return false;
}
}

public boolean moveNext() throws Exception
{
boolean hasNext = m_iterator.hasNext();
if (hasNext)
{
m_currentCustomer = (Customer) m_iterator.next();
}
return hasNext;
}
}


#13

Please provide the template document also. It will help us to reproduce the problem.


#14

OK, please find a word document attached. This is my template doc. The source code I'm using is below.

public class DataSourceExampleSolutionTest
{

public static void main(String[] args) throws Exception
{
DataSourceExampleSolutionTest me = new DataSourceExampleSolutionTest();
me.exampleCode();
}

private void exampleCode() throws Exception
{
// get the customer DAO class responsible for all DB access related to
// customers
CustomerDAO custDAO = new CustomerDAO();

// get a list of customers that we want to send a letter to - lets say
// all VIP customers as an example
List customers = custDAO.getVIPCustomers();

// get the source word document
Document wordDoc = new Document("C:\\temp\\aspose\\asposeDataSourceTest.doc");

wordDoc.getMailMerge().execute(new CustomerDataSource(customers));

// save the target doc
wordDoc.save("C:\\temp\\aspose\\asposeGenerated.doc");

}

}

public class CustomerDataSource implements IMailMergeDataSource
{
List m_customers;
Iterator m_iterator;
Customer m_currentCustomer;

public CustomerDataSource(List customers)
{
m_customers = customers;
m_iterator = customers.iterator();
}

/**
* The name of the data source. Used by Aspose.Words only when executing mail
* merge with repeatable regions.
*/
public String getTableName() throws Exception
{
return "applications";
}

public boolean getValue(String fieldName, Object[] fieldValue) throws Exception
{
String field = null;
if ("Firstname".equals(fieldName))
{
field = m_currentCustomer.getFirstname();
}
else if ("Surname".equals(fieldName))
{
field = m_currentCustomer.getSurname();
}

if (field != null)
{
fieldValue = new String[]{field};
return true;
}
else
{
// field was not found
return false;
}
}

public boolean moveNext() throws Exception
{
boolean hasNext = m_iterator.hasNext();
if (hasNext)
{
m_currentCustomer = (Customer) m_iterator.next();
}
return hasNext;
}
}


public class CustomerDAO
{
List getVIPCustomers()
{
Customer c1 = new Customer();
c1.setFirstname("Bob");
c1.setSurname("Dylan");

Customer c2 = new Customer();
c2.setFirstname("Bruce");
c2.setSurname("Springstein");

ArrayList list = new ArrayList();
list.add(c1);
list.add(c2);

return list;
}
}

public class Customer
{
private String m_firstname;
private String m_surname;
public String getFirstname()
{
return m_firstname;
}
public void setFirstname(String firstname)
{
m_firstname = firstname;
}
public String getSurname()
{
return m_surname;
}
public void setSurname(String surname)
{
m_surname = surname;
}
}


#15

Somehow the document failed to attach. Please reattach it once more. It should appear as a link in the header of a post.


#16

How to implement getValue() requires a bit of clarification. Sorry there is no documentation in the API Reference yet, but we are working on it.

public boolean getValue(String fieldName, Object[] fieldValue) throws Exception

The second parameter Object[] fieldValue is the well known Java pattern for an "out" parameter. It is a pity that such a powerfull and popular language does not have "out" parameters. This is one of the reasons I never understood why Java is so popular. But sorry its not up to the point.

Aspose.Words mail merge engine calls getValue() like this:

//java-changed: array pointer for emulation of C# out param
Object[] dataValue = {null};
boolean isDataValueFound = mDataSource.getValue(dataFieldName, dataValue);

Then in your implementation of getValue() you need to assign the data value to the first element of the array. This way the field value will be passed "out" of the getValue() method.

//for example
fieldValue[0] = mResultSet.getObject(name);
return true;

You don't need to create an array of string like you do in your code. Just assign the value of the field to fieldValue[0]


#17

Hopefully it should be attached now. It doesn’t seem to work when I do ‘preview’ so I’ve tried a direct ‘post’.


#18

Cheers Roman, I've got my code working now. Thanks for all your help with this. For anyone interested, the code I'm using for my data source is below.

public class CustomerDataSource implements IMailMergeDataSource
{
List m_customers;
Iterator m_iterator;
Customer m_currentCustomer;

public CustomerDataSource(List customers)
{
m_customers = customers;
m_iterator = customers.iterator();
}

/**
* The name of the data source. Used by Aspose.Words only when executing mail
* merge with repeatable regions.
*/
public String getTableName() throws Exception
{
return "applications";
}

public boolean getValue(String fieldName, Object[] fieldValue) throws Exception
{
String field = null;
if ("Firstname".equals(fieldName))
{
field = m_currentCustomer.getFirstname();
}
else if ("Surname".equals(fieldName))
{
field = m_currentCustomer.getSurname();
}

if (field != null)
{
fieldValue[0] = field;
return true;
}
else
{
// field was not found
return false;
}
}

public boolean moveNext() throws Exception
{
boolean hasNext = m_iterator.hasNext();
if (hasNext)
{
m_currentCustomer = (Customer) m_iterator.next();
}
return hasNext;
}
}