How to Apply Custom Logic to Unmerged Regions

Contents
[ ]

There are some situations where completely removing unmerged regions from the document during Mail Merge is not desired or results in the document looking incomplete. This can occur when the absence of input data should be displayed to the user in the form of a message instead of the region being completely removed.

There are also times when the removal of the unused region on its own is not enough, for instance, if the region is preceded with a title or the region is contained in a table. If this region is unused then the title and table will still remain after the region is removed which will look out of place in the document.

This article provides a solution to manually define how unused regions in the document are handled. The base code for this functionality is supplied and can be easily reused in another project.

The logic to be applied to each region is defined inside a class that implements the IFieldMergingCallback interface. In the same way, a Mail Merge handler can be set up to control how each field is merged, this handler can be set up to perform actions on each field in an unused region or on the region as a whole. Within this handler, you can set the code to change the text of a region, remove nodes or empty rows and cells etc.

In this sample, we will be using the document displayed below. It contains nested regions and a region contained within a table.

apply-custom-logic-to-unmerged-regions-aspose-words-java

As a quick demonstration, we can execute a sample database on the sample document with the MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS flag enabled. This property will automatically remove unmerged regions from the document during a mail merge.

The data source includes two records for the StoreDetails region but purposely does have any data for the child ContactDetails regions for one of the records. Furthermore, the Suppliers region does not have any data rows either. This will cause unused regions to remain in the document. The result after merging the document with this data source is below.

merged-regions-aspose-words-java

As noted on the image you can see that the ContactDetails region for the second record and Suppliers regions have been automatically removed by the Mail Merge engine as they have no data. However, there are a few issues that make this output document look incomplete:

  • The ContactDetails region still leaves a paragraph with the text “Contact Details”.
  • In the same case there is no indication that there are no phone numbers, only a blank space which could lead to confusion.
  • The table and title related to the Suppliers region also remains after the region inside the table is removed.

The technique provided in this article demonstrates how to apply custom logic to each unmerged regions to avoid these issues.

The Solution

To manually apply logic to each unused region in the document we take advantage of features already available in Aspose.Words.

The Mail Merge engine provides a property to remove unused regions through the MailMergeCleanupOptions.RemoveUnusedRegions flag. This can be disabled so that such regions are left untouched during a mail merge. This allows us to leave the unmerged regions in the document and handle them manually ourselves instead.

We can then take advantage of the MailMerge.FieldMergingCallback property as a means to apply our own custom logic to these unmerged regions during Mail Merge through the use of a handler class implementing the IFieldMergingCallback interface.

This code within the handler class is the only class you will need to modify in order to control the logic applied to unmerged regions. The other code in this sample can be reused without modification in any project.

This sample project demonstrates this technique. It involves the following steps:

  1. Execute Mail Merge on the document using your data source. The MailMergeCleanupOptions.RemoveUnusedRegions flag is disabled for now we want the regions to remain so we can handle them manually. Any regions without data will be left unmerged in the document.
  2. Call the ExecuteCustomLogicOnEmptyRegions method. This method is provided in this sample. It performs actions which allow the specified handler to be called for each unmerged region. This method is reusable and can be copied unaltered to any project which requires it (along with any dependent methods).This method executes the following steps:
    1. Sets the handler specified by the user to the MailMerge.FieldMergingCallback property.
    2. Calls the CreateDataSourceFromDocumentRegions method which accepts the user’s Document and ArrayList containing regions names. This method will create a dummy data source containing tables for each unmerged region in the document.
    3. Executes Mail Merge on the document using the dummy data source. When Mail Merge is executed with this data source it allows the user-specified handler to be called for each unmerge region and the custom logic applied

The Code

The implementation for the ExecuteCustomLogicOnEmptyRegions method is found below. This method accepts several parameters:

  1. The Document object containing unmerged regions which are to be handled by the passed handler.
  2. The handler class which defines the logic to apply to unmerged regions. This handler must implement the IFieldMergingCallback interface.
  3. Through the use of the appropriate overload, the method can also accept a third parameter – a list of region names as strings. If this is specified then only region names remaining the document specified in the list will be manually handled. Other regions which are encountered will not be called by the handler and removed automatically. When the overload with only two parameters is specified, every remaining region in the document is included by the method to be handled manually.

Example

Shows how to execute custom logic on unused regions using the specified handler.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
/**
* Applies logic defined in the passed handler class to all unused regions
* in the document. This allows to manually control how unused regions are
* handled in the document.
*
* @param doc The document containing unused regions.
* @param handler The handler which implements the IFieldMergingCallback
* interface and defines the logic to be applied to each unmerged
* region.
*/
public static void executeCustomLogicOnEmptyRegions(Document doc, IFieldMergingCallback handler) throws Exception {
executeCustomLogicOnEmptyRegions(doc, handler, null); // Pass null to handle all regions found in the document.
}
/**
* Applies logic defined in the passed handler class to specific unused
* regions in the document as defined in regionsList. This allows to
* manually control how unused regions are handled in the document.
*
* @param doc The document containing unused regions.
* @param handler The handler which implements the IFieldMergingCallback
* interface and defines the logic to be applied to each unmerged
* region.
* @param regionsList A list of strings corresponding to the region names that are
* to be handled by the supplied handler class. Other regions
* encountered will not be handled and are removed automatically.
*/
public static void executeCustomLogicOnEmptyRegions(Document doc, IFieldMergingCallback handler, ArrayList regionsList) throws Exception {
// Certain regions can be skipped from applying logic to by not adding the table name inside the CreateEmptyDataSource method.
// Enable this cleanup option so any regions which are not handled by the user's logic are removed automatically.
doc.getMailMerge().setCleanupOptions(MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS);
// Set the user's handler which is called for each unmerged region.
doc.getMailMerge().setFieldMergingCallback(handler);
// Execute mail merge using the dummy dataset. The dummy data source contains the table names of
// each unmerged region in the document (excluding ones that the user may have specified to be skipped). This will allow the handler
// to be called for each field in the unmerged regions.
doc.getMailMerge().executeWithRegions(createDataSourceFromDocumentRegions(doc, regionsList));
}
/**
* A helper method that creates an empty Java disconnected ResultSet with
* the specified columns.
*/
private static ResultSet createCachedRowSet(String[] columnNames) throws Exception {
RowSetMetaDataImpl metaData = new RowSetMetaDataImpl();
metaData.setColumnCount(columnNames.length);
for (int i = 0; i < columnNames.length; i++) {
metaData.setColumnName(i + 1, columnNames[i]);
metaData.setColumnType(i + 1, java.sql.Types.VARCHAR);
}
CachedRowSet rowSet = RowSetProvider.newFactory().createCachedRowSet();
;
rowSet.setMetaData(metaData);
return rowSet;
}
/**
* A helper method that adds a new row with the specified values to a
* disconnected ResultSet.
*/
private static void addRow(ResultSet resultSet, String[] values) throws Exception {
resultSet.moveToInsertRow();
for (int i = 0; i < values.length; i++)
resultSet.updateString(i + 1, values[i]);
resultSet.insertRow();
// This "dance" is needed to add rows to the end of the result set properly.
// If I do something else then rows are either added at the front or the result
// set throws an exception about a deleted row during mail merge.
resultSet.moveToCurrentRow();
resultSet.last();
}

Example

Defines the method used to manually handle unmerged regions.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
/**
* Returns a DataSet object containing a DataTable for the unmerged regions
* in the specified document. If regionsList is null all regions found
* within the document are included. If an ArrayList instance is present the
* only the regions specified in the list that are found in the document are
* added.
*/
private static DataSet createDataSourceFromDocumentRegions(Document doc, ArrayList regionsList) throws Exception {
final String TABLE_START_MARKER = "TableStart:";
DataSet dataSet = new DataSet();
String tableName = null;
for (String fieldName : doc.getMailMerge().getFieldNames()) {
if (fieldName.contains(TABLE_START_MARKER)) {
tableName = fieldName.substring(TABLE_START_MARKER.length());
} else if (tableName != null) {
// Only add the table as a new DataTable if it doesn't already exists in the DataSet.
if (dataSet.getTables().get(tableName) == null) {
ResultSet resultSet = createCachedRowSet(new String[]{fieldName});
// We only need to add the first field for the handler to be called for the fields in the region.
if (regionsList == null || regionsList.contains(tableName)) {
addRow(resultSet, new String[]{"FirstField"});
}
dataSet.getTables().add(new DataTable(resultSet, tableName));
}
tableName = null;
}
}
return dataSet;
}

This method involves finding all unmerged regions in the document. This is accomplished using the MailMerge.GetFieldNames method. This method returns all merge fields in the document, including the region start and end markers (represented by merge fields with the prefix TableStart or TableEnd).

When a TableStart merge field is encountered this is added as a new DataTable to the DataSet. Since a region may appear more than once (for example because it is a nested region where the parent region has been merged with multiple records), the table is only created and added if it does not already exist in the DataSet.

When an appropriate region start has been found and added to the database, the next field (which corresponds to the first field in the region) is added to the DataTable. Only the first field is required to be added for each field in the region to be merged and passed to the handler.

We also set the field value of the first field to “FirstField” to make it easier to apply logic to the first or other fields in the region. By including this it means it is not necessary to hard-code the name of the first field or implements extra code to check if the current field is the first in the handler code.

The code below demonstrates how this system works. The document shown at the start of this article is remerged with the same data source but this time, the unused regions are handled by custom code.

Example

Shows how to handle unmerged regions after Mail Merge with user-defined code.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
// Open the document.
Document doc = new Document(dataDir + "TestFile.doc");
// Create a data source which has some data missing.
// This will result in some regions that are merged and some that remain after executing mail merge.
DataSet data = getDataSource();
// Make sure that we have not set the removal of any unused regions as we will handle them manually.
// We achieve this by removing the RemoveUnusedRegions flag from the cleanup options by using the AND and NOT bitwise operators.
doc.getMailMerge().setCleanupOptions(doc.getMailMerge().getCleanupOptions() & ~MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS);
// Execute mail merge. Some regions will be merged with data, others left unmerged.
doc.getMailMerge().executeWithRegions(data);
// The regions which contained data now would of been merged. Any regions which had no data and were
// not merged will still remain in the document.
Document mergedDoc = doc.deepClone(); //ExSkip
// Apply logic to each unused region left in the document using the logic set out in the handler.
// The handler class must implement the IFieldMergingCallback interface.
executeCustomLogicOnEmptyRegions(doc, new EmptyRegionsHandler());
// Save the output document to disk.
doc.save(dataDir + "TestFile.CustomLogicEmptyRegions1 Out.doc");

The code performs different operations based on the name of the region retrieved using the FieldMergingArgs.TableName property. Note that depending upon your document and regions you can code the handler to run logic dependent on each region or code which applies to every unmerged region in the document or a combination of both.

The logic for the ContactDetails region involves changing the text of each field in the ContactDetails region with an appropriate message stating that there is no data. The names of each field are matched within the handler using the FieldMergingArgs.FieldName property.

A similar process is applied to the Suppliers region with the addition of extra code to handle the table which contains the region. The code will check if the region is contained within a table (as it may have already been removed). If it is, it will remove the entire table from the document as well as the paragraph which precedes it as long as it is formatted with a heading style e.g “Heading 1”.

Example

Shows how to define custom logic in a handler implementing IFieldMergingCallback that is executed for unmerged regions in the document.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
public static class EmptyRegionsHandler implements IFieldMergingCallback {
/**
* Called for each field belonging to an unmerged region in the
* document.
*/
public void fieldMerging(FieldMergingArgs args) throws Exception {
// Change the text of each field of the ContactDetails region individually.
if ("ContactDetails".equals(args.getTableName())) {
// Set the text of the field based off the field name.
if ("Name".equals(args.getFieldName()))
args.setText("(No details found)");
else if ("Number".equals(args.getFieldName()))
args.setText("(N/A)");
}
// Remove the entire table of the Suppliers region. Also check if the previous paragraph
// before the table is a heading paragraph and if so remove that too.
if ("Suppliers".equals(args.getTableName())) {
Table table = (Table) args.getField().getStart().getAncestor(NodeType.TABLE);
// Check if the table has been removed from the document already.
if (table.getParentNode() != null) {
// Try to find the paragraph which precedes the table before the table is removed from the document.
if (table.getPreviousSibling() != null && table.getPreviousSibling().getNodeType() == NodeType.PARAGRAPH) {
Paragraph previousPara = (Paragraph) table.getPreviousSibling();
if (isHeadingParagraph(previousPara))
previousPara.remove();
}
table.remove();
}
}
}
/**
* Returns true if the paragraph uses any Heading style e.g Heading 1 to
* Heading 9
*/
private boolean isHeadingParagraph(Paragraph para) throws Exception {
return (para.getParagraphFormat().getStyleIdentifier() >= StyleIdentifier.HEADING_1 && para.getParagraphFormat().getStyleIdentifier() <= StyleIdentifier.HEADING_9);
}
public void imageFieldMerging(ImageFieldMergingArgs args) throws Exception {
// Do Nothing
}
}

The result of the above code is shown below. The unmerged fields within the first region are replaced with informative text and the removal of the table and heading allows the document to look complete.

apply-custom-logic-to-unmerged-regions-aspose-words-java-2

The code which removes the parent table could also be made to run on every unused region instead of just a specific region by removing the check for the table name. In this case, if any region inside a table was not merged with any data, both the region and the container table will be automatically removed as well.

We can insert different code in the handler to control how unmerged regions are handled. Using the code below in the handler instead will change the text in the first paragraph of the region to a helpful message while any subsequent paragraphs in the region are removed. These other paragraphs are removed as they would remain in the region after merging our message.

The replacement text is merged into the first field by setting the specified text into the FieldMergingArgs.Text property. The text from this property is merged into the field by the Mail Merge engine.

The code applies this for only the first field in the region by checking the FieldMergingArgs.FieldValue property. The field value of the first field in the region is marked with “FirstField” . This makes this type of logic easier to implement over many regions as no extra code is required.

Example

Shows how to replace an unused region with a message and remove extra paragraphs.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
// Store the parent paragraph of the current field for easy access.
Paragraph parentParagraph = args.getField().getStart().getParentParagraph();
// Define the logic to be used when the ContactDetails region is encountered.
// The region is removed and replaced with a single line of text stating that there are no records.
if ("ContactDetails".equals(args.getTableName())) {
// Called for the first field encountered in a region. This can be used to execute logic on the first field
// in the region without needing to hard code the field name. Often the base logic is applied to the first field and
// different logic for other fields. The rest of the fields in the region will have a null FieldValue.
if ("FirstField".equals(args.getFieldValue())) {
FindReplaceOptions opts = new FindReplaceOptions();
opts.setMatchCase(false);
opts.setFindWholeWordsOnly(false);
// Remove the "Name:" tag from the start of the paragraph
parentParagraph.getRange().replace("Name:", "", opts);
// Set the text of the first field to display a message stating that there are no records.
args.setText("No records to display");
} else {
// We have already inserted our message in the paragraph belonging to the first field. The other paragraphs in the region
// will still remain so we want to remove these. A check is added to ensure that the paragraph has not already been removed.
// which may happen if more than one field is included in a paragraph.
if (parentParagraph.getParentNode() != null)
parentParagraph.remove();
}
}

The resulting document after the code above has been executed is shown below. The unused region is replaced with a message stating that there are no records to display.

apply-custom-logic-to-unmerged-regions-aspose-words-java-3

As another example, we can insert the code below in place of the code originally handling the SuppliersRegion . This will display a message within the table and merge the cells instead of removing the table from the document. Since the region resides within a table with multiple cells, it looks nicer to have the cells of the table merged together and the message centered.

Example

Shows how to merge all the parent cells of an unused region and display a message within the table.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
// Replace the unused region in the table with a "no records" message and merge all cells into one.
if ("Suppliers".equals(args.getTableName())) {
if ("FirstField".equals(args.getFieldValue())) {
// We will use the first paragraph to display our message. Make it centered within the table. The other fields in other cells
// within the table will be merged and won't be displayed so we don't need to do anything else with them.
parentParagraph.getParagraphFormat().setAlignment(ParagraphAlignment.CENTER);
args.setText("No records to display");
}
// Merge the cells of the table together.
Cell cell = (Cell) parentParagraph.getAncestor(NodeType.CELL);
if (cell != null) {
if (cell.isFirstCell())
cell.getCellFormat().setHorizontalMerge(CellMerge.FIRST); // If this cell is the first cell in the table then the merge is started using "CellMerge.First".
else
cell.getCellFormat().setHorizontalMerge(CellMerge.PREVIOUS); // Otherwise the merge is continued using "CellMerge.Previous".
}
}

The resulting document after the code above has been executed is shown below.

apply-custom-logic-to-unmerged-regions-aspose-words-java-4

Finally, we can call the ExecuteCustomLogicOnEmptyRegions method and specify the table names that should be handled within our handler method, while specifying others to be automatically removed.

Example

Shows how to specify only the ContactDetails region to be handled through the handler class.

// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java
ArrayList<String> regions = new ArrayList<String>();
regions.add("ContactDetails");
executeCustomLogicOnEmptyRegions(doc, new EmptyRegionsHandler(), regions);

Calling this overload with the specified ArrayList will create the data source which only contains data rows for the specified regions. Regions other than the ContactDetails region will not be handled and will be removed automatically by the Mail Merge engine instead. The result of the above call using the code in our original handler is shown below. 

apply-custom-logic-to-unmerged-regions-aspose-words-java-5