Bug 569165 - [Model2Doc][Docx] Model2Doc should provide a way to merge cells in tables

    * Add isCellMergedWithRightCell and isCellMergedWithBottomCell NamedStyles
    * Take those styles in account in the Docx transcription

Signed-off-by: Pauline DEVILLE <pauline.deville@cea.fr>
Change-Id: Iaae6d0d05964253d6444fe37b35e9330ed59bc46
diff --git a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/.classpath b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/.classpath
index 9909fb1..04c2c97 100755
--- a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/.classpath
+++ b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/.classpath
@@ -8,5 +8,6 @@
 	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
 	<classpathentry kind="src" path="src-gen"/>
 	<classpathentry kind="src" path="api"/>
+	<classpathentry kind="src" path="src"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/api/org/eclipse/papyrus/model2doc/core/styles/NamedStyleConstants.java b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/api/org/eclipse/papyrus/model2doc/core/styles/NamedStyleConstants.java
new file mode 100755
index 0000000..8eca09b
--- /dev/null
+++ b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/api/org/eclipse/papyrus/model2doc/core/styles/NamedStyleConstants.java
@@ -0,0 +1,46 @@
+/*****************************************************************************
+ * Copyright (c) 2020 CEA LIST and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *   Pauline DEVILLE (CEA LIST) pauline.deville@cea.fr - Initial API and implementation
+ *
+ *****************************************************************************/
+
+package org.eclipse.papyrus.model2doc.core.styles;
+
+/**
+ * This class list every style available
+ */
+public final class NamedStyleConstants {
+
+	/**
+	 * Merge the cell with the cell just in the right
+	 *
+	 * This style can be apply on Cell
+	 * This style wait for a boolean value
+	 */
+	public static final String MERGED_WITH_RIGHT_CELL = "mergedWithRightCell"; //$NON-NLS-1$
+
+	/**
+	 * Merge the cell with the cell just in the bottom
+	 *
+	 * This style can be apply on Cell
+	 * This style wait for a boolean value
+	 */
+	public static final String MERGED_WITH_BOTTOM_CELL = "mergedWithBottomCell"; //$NON-NLS-1$
+
+	/**
+	 * Constructor.
+	 *
+	 */
+	private NamedStyleConstants() {
+		// avoid instantiation
+	}
+}
diff --git a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/build.properties b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/build.properties
index 9a0a3cf..83c8710 100755
--- a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/build.properties
+++ b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/build.properties
@@ -19,6 +19,7 @@
                icons/
 jars.compile.order = .
 source.. = src-gen/,\
-           api/
+           api/,\
+           src/
 output.. = bin/
 src.includes = about.html
diff --git a/plugins/core/org.eclipse.papyrus.model2doc.core.styles/api/org/eclipse/papyrus/model2doc/core/styles/internal/operations/NamedStyleOperations.java b/plugins/core/org.eclipse.papyrus.model2doc.core.styles/src/org/eclipse/papyrus/model2doc/core/styles/internal/operations/NamedStyleOperations.java
similarity index 100%
rename from plugins/core/org.eclipse.papyrus.model2doc.core.styles/api/org/eclipse/papyrus/model2doc/core/styles/internal/operations/NamedStyleOperations.java
rename to plugins/core/org.eclipse.papyrus.model2doc.core.styles/src/org/eclipse/papyrus/model2doc/core/styles/internal/operations/NamedStyleOperations.java
diff --git a/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/false.jpg b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/false.jpg
new file mode 100755
index 0000000..12080fc
--- /dev/null
+++ b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/false.jpg
Binary files differ
diff --git a/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/true.jpg b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/true.jpg
new file mode 100755
index 0000000..8f88f4f
--- /dev/null
+++ b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/images/devDoc/true.jpg
Binary files differ
diff --git a/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/model2doc-devDoc.mediawiki b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/model2doc-devDoc.mediawiki
index 23253cc..868200f 100755
--- a/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/model2doc-devDoc.mediawiki
+++ b/plugins/doc/org.eclipse.papyrus.model2doc.doc/src/site/mediawiki/model2doc-devDoc.mediawiki
@@ -157,4 +157,36 @@
 =How to contribute a new Document generator=
 This generator is in charge to generate the final document from a DocumentStructure.
 You must create a new class implementing <code>org.eclipse.papyrus.model2doc.emf.structure2document.generator.IStructure2DocumentGenerator</code>.
-This class must be registered with the extension point <code>org.eclipse.papyrus.model2doc.emf.structure2document.documentgenerator</code>.
\ No newline at end of file
+This class must be registered with the extension point <code>org.eclipse.papyrus.model2doc.emf.structure2document.documentgenerator</code>.
+
+=Styles=
+The '''org.eclipse.papyrus.model2doc.core.styles''' plugin provide style metamodel. It define NamedStyle for Integer, String, Boolean and Double using single or multiple references.
+
+==How to add new NamedStyle==
+*You should add the new NamedStyle name in '''org.eclipse.papyrus.model2doc.core.styles.NamedStyleConstants'''
+*Update the transcription to use your new NamedStyle
+*Don't forget the update this documentation to be sure that the list of NamedStyles is updated
+
+==Existing NamedStyles==
+{| class="wikitable"
+! Name
+! Description
+! Value type
+! Applies to
+! Odt
+! Docx
+|-
+|mergedWithRightCell
+|Merge the cell with the cell located just to the right
+|Boolean
+|Cell
+|[[Image:images/devDoc/false.jpg]]
+|[[Image:images/devDoc/true.jpg]]
+|-
+|mergedWithBottomCell
+|Merge the cell with the cell located just to below
+|Boolean
+|Cell
+|[[Image:images/devDoc/false.jpg]]
+|[[Image:images/devDoc/true.jpg]]
+|}
diff --git a/plugins/docx/org.eclipse.papyrus.model2doc.docx/META-INF/MANIFEST.MF b/plugins/docx/org.eclipse.papyrus.model2doc.docx/META-INF/MANIFEST.MF
index f8aa988..000a9cc 100755
--- a/plugins/docx/org.eclipse.papyrus.model2doc.docx/META-INF/MANIFEST.MF
+++ b/plugins/docx/org.eclipse.papyrus.model2doc.docx/META-INF/MANIFEST.MF
@@ -14,6 +14,7 @@
  org.eclipse.papyrus.model2doc.core.generatorconfiguration;bundle-version="[0.8.0,1.0.0)",
  org.eclipse.papyrus.model2doc.core.builtintypes;bundle-version="[0.8.0,1.0.0)",
  org.eclipse.papyrus.model2doc.core.author;bundle-version="[0.7.0,1.0.0)",
+ org.eclipse.papyrus.model2doc.core.styles;bundle-version="[0.8.0,1.0.0)",
  org.apache.xmlbeans;bundle-version="[3.1.0,4.0.0)",
  org.apache.poi.ooxml.schemas;bundle-version="[4.1.0,5.0.0)",
  org.apache.poi;bundle-version="[4.1.0,5.0.0)"
diff --git a/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFDocument.java b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFDocument.java
index cdce22b..7921df3 100755
--- a/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFDocument.java
+++ b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFDocument.java
@@ -20,6 +20,7 @@
 
 import org.apache.poi.xwpf.usermodel.XWPFDocument;
 import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
 import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
 import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSimpleField;
 import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff;
@@ -72,4 +73,32 @@
 		toc.setDirty(STOnOff.TRUE); // FIXME a pop up appear when we open the document
 	}
 
+	/**
+	 * Create an empty table with one row and one column as default.
+	 *
+	 * @return a new table
+	 */
+	@Override
+	public XWPFTable createTable() {
+		XWPFTable table = new CustomXWPFTable(getDocument().getBody().addNewTbl(), this);
+		bodyElements.add(table);
+		tables.add(table);
+		return table;
+	}
+
+	/**
+	 * Create an empty table with a number of rows and cols specified
+	 *
+	 * @param rows
+	 * @param cols
+	 * @return table
+	 */
+	@Override
+	public XWPFTable createTable(int rows, int cols) {
+		XWPFTable table = new CustomXWPFTable(getDocument().getBody().addNewTbl(), this, rows, cols);
+		bodyElements.add(table);
+		tables.add(table);
+		return table;
+	}
+
 }
diff --git a/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFTable.java b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFTable.java
new file mode 100755
index 0000000..78aa88c
--- /dev/null
+++ b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/poi/CustomXWPFTable.java
@@ -0,0 +1,137 @@
+/*****************************************************************************
+ * Copyright (c) 2020 CEA LIST and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *   Pauline DEVILLE (CEA LIST) pauline.deville@cea.fr - Initial API and implementation
+ *
+ *****************************************************************************/
+
+package org.eclipse.papyrus.model2doc.docx.internal.poi;
+
+import org.apache.poi.xwpf.usermodel.IBody;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.eclipse.papyrus.model2doc.docx.Activator;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHMerge;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
+
+/**
+ *
+ */
+public class CustomXWPFTable extends XWPFTable {
+
+	/**
+	 * Constructor.
+	 *
+	 * @param table
+	 * @param part
+	 */
+	public CustomXWPFTable(CTTbl table, IBody part) {
+		super(table, part);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param table
+	 * @param part
+	 * @param row
+	 * @param col
+	 */
+	public CustomXWPFTable(CTTbl table, IBody part, int row, int col) {
+		super(table, part, row, col);
+	}
+
+	/**
+	 * Merge cells present between the startCell and the endCell, the content of the merge cell is the content of the startCell
+	 *
+	 * @param rowIndex
+	 *            the position of the row to merge
+	 * @param startCellIndex
+	 *            the position in the row of the first cell to merge
+	 * @param endCell
+	 *            the position in the row of the last cell to merge
+	 */
+	public void horizontalCellMerge(int rowIndex, int startCellIndex, int endCellIndex) {
+		// some check
+		if (rowIndex >= getNumberOfRows()) {
+			Activator.log.warn("The merge of cells is not possible since the range of cells is out of bounds"); //$NON-NLS-1$
+		}
+		if (startCellIndex >= getRow(rowIndex).getTableCells().size() || endCellIndex >= getRow(rowIndex).getTableCells().size()) {
+			Activator.log.warn("The merge of cells is not possible since the range of cells is out of bounds"); //$NON-NLS-1$
+		}
+
+		// merge
+		CTHMerge startMerge = CTHMerge.Factory.newInstance();
+		startMerge.setVal(STMerge.RESTART);
+		CTHMerge continueMerge = CTHMerge.Factory.newInstance();
+		continueMerge.setVal(STMerge.CONTINUE);
+
+		XWPFTableCell startCell = getRow(rowIndex).getCell(startCellIndex);
+		startCell.getCTTc().addNewTcPr().setHMerge(startMerge);
+
+		int index = startCellIndex + 1;
+		while (index <= endCellIndex) {
+			XWPFTableCell continueCell = getRow(rowIndex).getCell(index);
+			CTTcPr tcpr = continueCell.getCTTc().getTcPr();
+			if (tcpr == null) {
+				tcpr = continueCell.getCTTc().addNewTcPr();
+			}
+			tcpr.setHMerge(continueMerge);
+			index++;
+		}
+	}
+
+	/**
+	 * Merge cells present between the startCell and the endCell, the content of the merge cell is the content of the startCell
+	 *
+	 * @param columnIndex
+	 *            the position of the column to merge
+	 * @param startRowIndex
+	 *            the position of the first row to merge
+	 * @param endRowIndex
+	 *            the position of the last row to merge
+	 */
+	public void verticalCellMerge(int columnIndex, int startRowIndex, int endRowIndex) {
+		// some check
+		if (startRowIndex >= getNumberOfRows() || endRowIndex >= getNumberOfRows()) {
+			Activator.log.warn("The merge of cells is not possible since the range of cells is out of bounds"); //$NON-NLS-1$
+		}
+		if (columnIndex >= getRow(0).getTableCells().size()) {
+			Activator.log.warn("The merge of cells is not possible since the range of cells is out of bounds"); //$NON-NLS-1$
+		}
+
+		// merge
+		CTVMerge startMerge = CTVMerge.Factory.newInstance();
+		startMerge.setVal(STMerge.RESTART);
+		CTVMerge continueMerge = CTVMerge.Factory.newInstance();
+		continueMerge.setVal(STMerge.CONTINUE);
+
+		XWPFTableCell startCell = getRow(startRowIndex).getCell(columnIndex);
+		startCell.getCTTc().addNewTcPr().setVMerge(startMerge);
+
+		int index = startRowIndex + 1;
+		while (index <= endRowIndex) {
+			XWPFTableCell continueCell = getRow(index).getCell(columnIndex);
+			CTTcPr tcpr = continueCell.getCTTc().getTcPr();
+			if (tcpr == null) {
+				tcpr = continueCell.getCTTc().addNewTcPr();
+			}
+			tcpr.setVMerge(continueMerge);
+			index++;
+		}
+	}
+
+
+
+}
diff --git a/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/transcription/DocxTranscription.java b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/transcription/DocxTranscription.java
index 61beb57..63ac549 100755
--- a/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/transcription/DocxTranscription.java
+++ b/plugins/docx/org.eclipse.papyrus.model2doc.docx/src/org/eclipse/papyrus/model2doc/docx/internal/transcription/DocxTranscription.java
@@ -19,6 +19,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
@@ -42,12 +43,15 @@
 import org.eclipse.papyrus.model2doc.core.builtintypes.TextCell;
 import org.eclipse.papyrus.model2doc.core.generatorconfiguration.IDocumentGeneratorConfiguration;
 import org.eclipse.papyrus.model2doc.core.generatorconfiguration.operations.GeneratorConfigurationOperations;
+import org.eclipse.papyrus.model2doc.core.styles.BooleanNamedStyle;
+import org.eclipse.papyrus.model2doc.core.styles.NamedStyleConstants;
 import org.eclipse.papyrus.model2doc.core.transcription.CoverPage;
 import org.eclipse.papyrus.model2doc.core.transcription.ImageDescription;
 import org.eclipse.papyrus.model2doc.core.transcription.Transcription;
 import org.eclipse.papyrus.model2doc.docx.Activator;
 import org.eclipse.papyrus.model2doc.docx.Messages;
 import org.eclipse.papyrus.model2doc.docx.internal.poi.CustomXWPFDocument;
+import org.eclipse.papyrus.model2doc.docx.internal.poi.CustomXWPFTable;
 import org.eclipse.papyrus.model2doc.docx.internal.services.StyleServiceImpl;
 import org.eclipse.papyrus.model2doc.docx.internal.util.ImageUtils;
 import org.eclipse.papyrus.model2doc.docx.services.StyleService;
@@ -200,27 +204,118 @@
 		// create and fill the table
 		XWPFTable xwpfTable = document.createTable(rowsNumber, colNumbers);
 		Iterator<Row> rowIter = table.getRows().iterator();
-		int rowNumber = 0;
+		int rowIndex = 0;
 		while (rowIter.hasNext()) {
 			Row row = rowIter.next();
 			Iterator<Cell> cellIter = row.getCells().iterator();
-			int cellNumber = 0;
+			int cellIndex = 0;
 			while (cellIter.hasNext()) {
 				Cell cell = cellIter.next();
 				if (cell instanceof TextCell) {
 					TextCell textCell = (TextCell) cell;
-					xwpfTable.getRow(rowNumber).getCell(cellNumber).setText(textCell.getText());
+					xwpfTable.getRow(rowIndex).getCell(cellIndex).setText(textCell.getText());
 				}
-				cellNumber++;
+				cellIndex++;
 			}
-			rowNumber++;
+			rowIndex++;
 		}
 
-		// apply style
+		// apply styles
+		rowIter = table.getRows().iterator();
+		List<Cell> verticalMergedCells = new ArrayList<>();
+		while (rowIter.hasNext()) {
+			Row row = rowIter.next();
+			Iterator<Cell> cellIter = row.getCells().iterator();
+			while (cellIter.hasNext()) {
+				Cell cell = cellIter.next();
+
+				// Horizontal cell merge
+				BooleanNamedStyle style = (BooleanNamedStyle) cell.getNamedStyle(NamedStyleConstants.MERGED_WITH_RIGHT_CELL);
+				if (null != style && style.isValue()) {
+					Cell lastMergedCell = findLastHorizontalMergedCell(cellIter);
+
+					if (lastMergedCell != null && xwpfTable instanceof CustomXWPFTable) {
+						((CustomXWPFTable) xwpfTable).horizontalCellMerge(
+								table.getRows().indexOf(row),
+								row.getCells().indexOf(cell),
+								row.getCells().indexOf(lastMergedCell));
+					}
+				}
+
+				// Vertical cell merge
+				style = (BooleanNamedStyle) cell.getNamedStyle(NamedStyleConstants.MERGED_WITH_BOTTOM_CELL);
+				if (null != style && style.isValue()) {
+					if (false == verticalMergedCells.contains(cell)) { // otherwise it is already managed
+						Row lastMergedRow = findVerticalMergedCells(table, row, cell, verticalMergedCells);
+						if (lastMergedRow != null && xwpfTable instanceof CustomXWPFTable) {
+							((CustomXWPFTable) xwpfTable).verticalCellMerge(
+									row.getCells().indexOf(cell),
+									table.getRows().indexOf(row),
+									table.getRows().indexOf(lastMergedRow));
+						}
+					}
+				}
+			}
+		}
+
 		xwpfTable.setWidthType(TableWidthType.PCT); // resize table to use the page width
 		styleService.applyTableStyle(xwpfTable, document, table);
 	}
 
+	/**
+	 * Find the first cell following the iterator which does not have the {@link NamedStyleConstants.MERGED_WITH_RIGHT_CELL} style
+	 *
+	 * @param cellIter
+	 *            the start of the research
+	 * @return the last cell concerning by the merge
+	 */
+	private Cell findLastHorizontalMergedCell(Iterator<Cell> cellIter) {
+		while (cellIter.hasNext()) {
+			Cell cell = cellIter.next();
+			BooleanNamedStyle style = (BooleanNamedStyle) cell.getNamedStyle(NamedStyleConstants.MERGED_WITH_RIGHT_CELL);
+			if (null != style && style.isValue()) {
+				continue;
+			} else {
+				return cell;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * This method add every cells that must be merged with the firstCell (according to the {@link NamedStyleConstants.MERGED_WITH_BOTTOM_CELL} style)
+	 * to the mergedCells collection then it return the row of the laster merged cell
+	 *
+	 * @param table
+	 *            the table
+	 * @param row
+	 *            the row of the first cell of the merge
+	 * @param firstCell
+	 *            the first cell of the merge
+	 * @param mergedCells
+	 *            the list of merged cells
+	 * @return the row of the last merged cell
+	 */
+	private Row findVerticalMergedCells(final AbstractTable table, final Row row, final Cell firstCell, List<Cell> mergedCells) {
+		int columnIndex = row.getCells().indexOf(firstCell);
+		int rowIndex = table.getRows().indexOf(row) + 1;
+
+		while (rowIndex < table.getRowsNumber()) {
+			Cell cell = table.getRows().get(rowIndex).getCells().get(columnIndex);
+
+			BooleanNamedStyle style = (BooleanNamedStyle) cell.getNamedStyle(NamedStyleConstants.MERGED_WITH_BOTTOM_CELL);
+			if (null != style && style.isValue()) {
+				mergedCells.add(cell);
+			} else {
+				mergedCells.add(cell);
+				return table.getRows().get(rowIndex);
+			}
+			rowIndex++;
+		}
+
+		return null;
+	}
+
 	@Override
 	public void insertFile(IFileReference fileReference) {
 		// TODO Auto-generated method stub