Как применить пользовательскую логику к несоединенным областям

Contents
[ ]

В некоторых ситуациях полное удаление несоединенных областей из документа во время Mail Merge нежелательно или приводит к тому, что документ выглядит неполным. Это может произойти, когда пользователю вместо полного удаления области следует отобразить отсутствие входных данных в виде сообщения.

Бывают также случаи, когда одного удаления неиспользуемого региона недостаточно, например, если перед регионом стоит заголовок или регион содержится в таблице. Если эта область не используется, то заголовок и таблица все равно останутся после удаления области, что будет выглядеть неуместно в документе.

В этой статье предлагается решение, позволяющее вручную определить, как обрабатываются неиспользуемые области в документе. Базовый код для этой функции предоставляется и может быть легко использован повторно в другом проекте.

Логика, применяемая к каждой области, определяется внутри класса, реализующего интерфейс IFieldMergingCallback. Точно так же можно настроить обработчик Mail Merge для управления объединением каждого поля, этот обработчик можно настроить для выполнения действий с каждым полем в неиспользуемой области или с регионом в целом. В этом обработчике вы можете задать код для изменения текста области, удаления узлов или пустых строк и ячеек и т.д.

В этом примере мы будем использовать документ, показанный ниже. Он содержит вложенные области и область, содержащуюся в таблице.

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

В качестве краткой демонстрации мы можем запустить образец базы данных в образце документа с включенным флагом MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS. Это свойство автоматически удалит несоединенные области из документа в течение mail merge.

Источник данных содержит две записи для области StoreDetails, но намеренно содержит какие-либо данные для дочерних областей ContactDetails для одной из записей. Кроме того, в области Suppliers также нет строк данных. Это приведет к тому, что в документе останутся неиспользуемые области. Результат после объединения документа с этим источником данных приведен ниже.

merged-regions-aspose-words-java

Как показано на рисунке, вы можете видеть, что область ContactDetails для второй записи и области Suppliers были автоматически удалены обработчиком Mail Merge, поскольку в них нет данных. Однако есть несколько проблем, из-за которых этот выходной документ выглядит неполным:

  • В области ContactDetails по-прежнему остается абзац с текстом “Контактные данные”.
  • В том же случае нет никаких указаний на отсутствие телефонных номеров, только пробел, который может привести к путанице.
  • Таблица и заголовок, относящиеся к области Suppliers, также остаются после удаления области внутри таблицы.

Методика, представленная в этой статье, демонстрирует, как применить пользовательскую логику к каждой несоединенной области, чтобы избежать этих проблем.

Решение

Чтобы вручную применить логику к каждой неиспользуемой области документа, мы используем возможности, уже доступные в Aspose.Words.

Механизм Mail Merge предоставляет свойство удалять неиспользуемые области с помощью флага MailMergeCleanupOptions.RemoveUnusedRegions. Это можно отключить, чтобы такие области оставались нетронутыми в течение mail merge. Это позволяет нам оставлять не объединенные области в документе и обрабатывать их вручную самостоятельно.

Затем мы можем воспользоваться свойством MailMerge.FieldMergingCallback как средством применения нашей собственной пользовательской логики к этим несоединенным областям во время Mail Merge с помощью класса-обработчика, реализующего интерфейс IFieldMergingCallback.

Этот код в классе handler - единственный класс, который вам нужно будет изменить, чтобы управлять логикой, применяемой к несоединенным областям. Другой код в этом примере можно повторно использовать без изменений в любом проекте.

Этот примерный проект демонстрирует эту технику. Он включает в себя следующие шаги:

  1. Выполните Mail Merge в документе, используя ваш источник данных. Флаг MailMergeCleanupOptions.RemoveUnusedRegions отключен, пока мы хотим, чтобы области оставались, чтобы мы могли обрабатывать их вручную. Все области без данных будут удалены из документа.
  2. Вызовите метод ExecuteCustomLogicOnEmptyRegions. Этот метод представлен в этом примере. Он выполняет действия, которые позволяют вызывать указанный обработчик для каждой выделенной области. Этот метод можно использовать повторно и без изменений скопировать в любой проект, который его требует (вместе с любыми зависимыми методами).Этот метод выполняет следующие действия:
    1. Присваивает свойству MailMerge.FieldMergingCallback обработчика, указанного пользователем.
    2. Вызывает метод CreateDataSourceFromDocumentRegions, который принимает пользовательские имена областей, содержащие Document и ArrayList. Этот метод создаст фиктивный источник данных, содержащий таблицы для каждой выделенной области в документе.
    3. Выполняет Mail Merge для документа, используя фиктивный источник данных. Когда Mail Merge выполняется с этим источником данных, это позволяет вызывать указанный пользователем обработчик для каждой области разделения и применять пользовательскую логику

Код

Ниже приведена реализация метода ExecuteCustomLogicOnEmptyRegions. Этот метод принимает несколько параметров:

  1. Объект Document, содержащий несоединенные области, которые должны быть обработаны переданным обработчиком.
  2. Класс обработчика, который определяет логику, применяемую к несоединенным областям. Этот обработчик должен реализовывать IFieldMergingCallback интерфейс.
  3. Используя соответствующую перегрузку, метод может также принимать третий параметр – список названий регионов в виде строк. Если это указано, то вручную будут обрабатываться только названия регионов, остающиеся в документе, указанном в списке. Другие обнаруженные области не будут вызываться обработчиком и будут удалены автоматически. Если задана перегрузка только с двумя параметрами, каждая оставшаяся область в документе включается с помощью метода, который будет обрабатываться вручную.

Пример

Показывает, как выполнить пользовательскую логику в неиспользуемых областях с помощью указанного обработчика.

// 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();
}

Пример

Определяет метод, используемый для ручной обработки несоединенных областей.

// 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;
}

Этот метод предполагает поиск всех несоединенных областей в документе. Для этого используется метод MailMerge.GetFieldNames. Этот метод возвращает все объединяемые поля в документе, включая маркеры начала и конца области (представленные объединяемыми полями с префиксом TableStart или TableEnd).

При обнаружении поля слияния TableStart оно добавляется как новое DataTable к DataSet. Поскольку область может отображаться более одного раза (например, потому что это вложенная область, в которой родительская область была объединена с несколькими записями), таблица создается и добавляется только в том случае, если она еще не существует в DataSet.

Когда подходящее начало региона найдено и добавлено в базу данных, к DataTable добавляется следующее поле (соответствующее первому полю в регионе). Для каждого поля в регионе, которое должно быть объединено и передано обработчику, требуется добавить только первое поле.

Мы также установили значение первого поля равным “FirstField”, чтобы упростить применение логики к первому или другим полям в регионе. Включение этого параметра означает, что нет необходимости жестко кодировать имя первого поля или реализовывать дополнительный код для проверки того, является ли текущее поле первым в коде обработчика.

Приведенный ниже код демонстрирует, как работает эта система. Документ, показанный в начале этой статьи, повторно объединен с тем же источником данных, но на этот раз неиспользуемые области обрабатываются пользовательским кодом.

Пример

Показывает, как обрабатывать несоединенные области после Mail Merge с помощью пользовательского кода.

// 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");

Код выполняет различные операции на основе названия региона, полученного с помощью свойства FieldMergingArgs.TableName. Обратите внимание, что в зависимости от вашего документа и регионов вы можете запрограммировать обработчик для запуска логики, зависящей от каждого региона, или кода, который применяется к каждому отдельному региону в документе, или их комбинации.

Логика для области ContactDetails заключается в изменении текста каждого поля в области ContactDetails с соответствующим сообщением об отсутствии данных. Имена каждого поля сопоставляются в обработчике с помощью свойства FieldMergingArgs.FieldName.

Аналогичный процесс применяется к области Suppliers с добавлением дополнительного кода для обработки таблицы, содержащей эту область. Код проверит, содержится ли область в таблице (поскольку она, возможно, уже была удалена). Если это так, то из документа будет удалена вся таблица целиком, а также предшествующий ей абзац, если он отформатирован в стиле заголовка, например “Heading 1”.

Пример

Показывает, как определить пользовательскую логику в обработчике, реализующем IFieldMergingCallback, который выполняется для несвязанных областей в документе.

// 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
}
}

Результат выполнения приведенного выше кода показан ниже. Не объединенные поля в первой области заменены информативным текстом, а удаление таблицы и заголовка позволяет документу выглядеть завершенным.

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

Код, который удаляет родительскую таблицу, также можно было бы заставить выполняться в каждом неиспользуемом регионе, а не только в определенном регионе, удалив проверку имени таблицы. В этом случае, если какой-либо регион внутри таблицы не был объединен с какими-либо данными, как регион, так и таблица-контейнер также будут автоматически удалены.

Мы можем вставить другой код в обработчик, чтобы управлять обработкой разделенных областей. Использование приведенного ниже кода в обработчике вместо этого изменит текст в первом абзаце области на полезное сообщение, в то время как все последующие абзацы в области будут удалены. Эти другие абзацы удалены, поскольку они останутся в регионе после объединения нашего сообщения.

Заменяющий текст вводится в первое поле путем установки указанного текста в свойстве FieldMergingArgs.Text. Текст из этого свойства вводится в поле с помощью механизма Mail Merge.

Код применяет это только к первому полю в регионе, проверяя свойство FieldMergingArgs.FieldValue. Значение первого поля в регионе помечается как “FirstField”. Это упрощает реализацию этого типа логики во многих регионах, поскольку не требуется никакого дополнительного кода.

Пример

Показывает, как заменить неиспользуемую область сообщением и удалить лишние абзацы.

// 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();
}
}

Полученный документ после выполнения приведенного выше кода показан ниже. Неиспользуемая область заменяется сообщением о том, что в ней нет записей для отображения.

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

В качестве другого примера, мы можем вставить приведенный ниже код вместо кода, который изначально обрабатывал SuppliersRegion. Это приведет к отображению сообщения внутри таблицы и объединению ячеек вместо удаления таблицы из документа. Поскольку область находится в таблице с несколькими ячейками, лучше объединить ячейки таблицы вместе и расположить сообщение по центру.

Пример

Показывает, как объединить все родительские ячейки неиспользуемой области и отобразить сообщение в таблице.

// 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".
}
}

Результирующий документ после выполнения приведенного выше кода показан ниже.

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

Наконец, мы можем вызвать метод ExecuteCustomLogicOnEmptyRegions и указать имена таблиц, которые должны обрабатываться в нашем методе-обработчике, указав при этом другие, которые будут автоматически удалены.

Пример

Показывает, как указать только область ContactDetails, которая будет обрабатываться с помощью класса handler.

// 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);

При вызове этой перегрузки с указанным значением ArrayList будет создан источник данных, содержащий только строки данных для указанных областей. Области, отличные от области ContactDetails, обрабатываться не будут и вместо этого будут автоматически удалены механизмом Mail Merge. Результат приведенного выше вызова с использованием кода из нашего исходного обработчика показан ниже.

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