package org.fenixedu.oddjet; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.fenixedu.oddjet.exception.AttributeChainResolutionFailureException; import org.fenixedu.oddjet.exception.DocumentLoadException; import org.fenixedu.oddjet.exception.DocumentSaveException; import org.fenixedu.oddjet.exception.IllegalTableCallRepresentationException; import org.fenixedu.oddjet.exception.IllegalTemplateDataSourceNameException; import org.fenixedu.oddjet.exception.IllegalTemplateParameterNameException; import org.fenixedu.oddjet.exception.OpenOfficeConnectionException; import org.fenixedu.oddjet.table.TableCall; import org.fenixedu.oddjet.table.TableConfiguration; import org.fenixedu.oddjet.table.TableConfiguration.ContentDirection; import org.fenixedu.oddjet.table.TableConfiguration.ContentStructure; import org.fenixedu.oddjet.table.TableConfiguration.LastBorderSourceSection; import org.fenixedu.oddjet.table.TableCoordinate; import org.fenixedu.oddjet.table.TableData; import org.fenixedu.oddjet.utils.OpenOfficePrintingService; import org.fenixedu.oddjet.utils.PrintUtils; import org.odftoolkit.odfdom.dom.OdfMetaDom; import org.odftoolkit.odfdom.dom.element.OdfStylableElement; import org.odftoolkit.odfdom.dom.style.props.OdfStyleProperty; import org.odftoolkit.simple.TextDocument; import org.odftoolkit.simple.common.field.Fields; import org.odftoolkit.simple.common.field.VariableField; import org.odftoolkit.simple.style.Border; import org.odftoolkit.simple.style.StyleTypeDefinitions.CellBordersType; import org.odftoolkit.simple.table.Cell; import org.odftoolkit.simple.table.CellRange; import org.odftoolkit.simple.table.Table; import org.odftoolkit.simple.text.Paragraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Contains a template file along with the teplate's data and locale, allowing creating instances of the original template * document with the contained data and locale, printing them and saving the results. * * @author Gil Lacerda (gil.lacerda@tecnico.ulisboa.pt) * */ public class Template { /** The bytes of the template document file. */ private byte[] bytes; /** The file path to the template document file. */ private String path; /** The locale of the template. */ private Locale locale; /** Map of template data parameters. */ final private Map<String, Object> dataParameters = new HashMap<String, Object>(); /** Map of template table data sources. */ final private Map<String, TableData> tableDataSources = new HashMap<String, TableData>(); /** The regex string to match parameter attribute access. */ private static final String ATTRIBUTE_ACCESS_REGEX = "\\."; private static final Logger logger = LoggerFactory.getLogger(Template.class); /** * Constructs a Template with no associated template file and with the default locale. */ public Template() { this(Locale.getDefault()); } /** * Constructs a Template with no associated template file and with the given locale. * * @param locale the template's locale. */ public Template(Locale locale) { setLocale(locale); } /** * Constructs a Template reading a template file from a given file path and with the given locale. * * @param filePath the path to the template file. * @param locale the template's locale. * @throws DocumentLoadException if the file at filePath could not be read. */ public Template(String filePath, Locale locale) throws DocumentLoadException { setDocument(filePath); setLocale(locale); } /** * Constructs a Template reading a template file from a given file path and with the default locale. * * @param filePath the path to the template file. * @throws DocumentLoadException if the file at filePath could not be read. */ public Template(String filePath) throws DocumentLoadException { this(filePath, Locale.getDefault()); } /** * Constructs a Template reading a given template file and with the given locale. * * @param file the template file. * @param locale the template's locale. * @throws DocumentLoadException if the file could not be read. */ public Template(File file, Locale locale) throws DocumentLoadException { setDocument(file); setLocale(locale); } /** * Constructs a Template reading a given template file and with the default locale. * * @param file the template file. * @throws SecurityException if read access to the file is denied. * @throws DocumentLoadException if the file could not be read. */ public Template(File file) throws DocumentLoadException { this(file, Locale.getDefault()); } /** * Constructs a Template reading the template file from a given InputStream and with a given locale. * * @param fileStream the stream to read the template file. * @param locale the template's locale. * @throws DocumentLoadException if the file could not be read from the stream. */ public Template(InputStream fileStream, Locale locale) throws DocumentLoadException { setDocument(fileStream); setLocale(locale); } /** * Constructs a Template reading the template file from a given InputStream and with the default locale. * * @param fileStream the stream to read the template file. * @throws DocumentLoadException if the file could not be read from the stream. */ public Template(InputStream fileStream) throws DocumentLoadException { this(fileStream, Locale.getDefault()); } /** * Constructs a Template from a template file byte array and with a given locale. * * @param fileContent the template file byte array. * @param locale the template's locale. */ public Template(byte[] fileContent, Locale locale) { this.bytes = fileContent; setLocale(locale); } /** * Constructs a Template from a template file byte array and with the default locale. * * @param fileContent the template file byte array. */ public Template(byte[] fileContent) { this(fileContent, Locale.getDefault()); } /** * Reads and sets the template document from the file at the given filePath. * * @param filePath the path to the template file. * @throws DocumentLoadException if the file at filePath could not be read. */ public void setDocument(String filePath) throws DocumentLoadException { setDocument(new File(filePath)); } /** * Reads and sets the template document from the given file. * * @param file the template file. * @throws DocumentLoadException if the file could not be read. */ public void setDocument(File file) throws DocumentLoadException { setPath(file.getAbsolutePath()); try { setDocument(new FileInputStream(file)); } catch (SecurityException | FileNotFoundException e) { throw new DocumentLoadException(e); } } /** * Reads and sets the template document from the given InputStream. * * @param fileStream the stream to read the template file. * @throws DocumentLoadException if the file could not be read from the stream. */ public void setDocument(InputStream fileStream) throws DocumentLoadException { try { this.bytes = IOUtils.toByteArray(fileStream); } catch (IOException e) { throw new DocumentLoadException(e); } } /** * @return a map with the current data parameters. This map cannot be used to add, modify or remove parameters. */ public Map<String, Object> getParameters() { return new HashMap<String, Object>(dataParameters); } /** * Adds or replaces a template data parameter. * * @param name the name of the data parameter. * @param value the data object for this parameter. * @throws IllegalTemplateParameterNameException if the supplied name contains the attribute access operator ".". */ public void addParameter(String name, Object value) throws IllegalTemplateParameterNameException { if (Pattern.compile(ATTRIBUTE_ACCESS_REGEX).matcher(name).find()) { throw new IllegalTemplateParameterNameException(name); } else { this.dataParameters.put(name, value); } } /** Removes all template data parameters. */ public void clearParameters() { this.dataParameters.clear(); } /** * Removes the template data parameter with the given name. * * @param name the name of the data parameter that is to be removed. */ public void removeParameter(String name) { this.dataParameters.remove(name); } /** * Adds or replaces a template table's data source. * * @param name the name of the table to contain this data. * @param value the object containing the table data. * @throws IllegalTemplateDataSourceNameException if the supplied name does not conform to the table source name notation */ public void addTableDataSource(String name, TableData value) throws IllegalTemplateDataSourceNameException { if (TableCall.isValidSourceName(name)) { this.tableDataSources.put(name, value); } else { throw new IllegalTemplateDataSourceNameException(name); } } /** Removes all template table data sources. */ public void clearTableDataSources() { this.tableDataSources.clear(); } /** * Removes the template table's data source for the table with the given name. * * @param name the name of the table whose data source is to be removed. */ public void removeTableDataSource(String name) { this.tableDataSources.remove(name); } /** * @return a map with the current table data sources. */ public Map<String, TableData> getTableDataSources() { return new HashMap<String, TableData>(tableDataSources); } /** * @return the template locale. */ public Locale getLocale() { return locale; } /** * @param locale the locale to be set. */ public void setLocale(Locale locale) { this.locale = locale; } /** * Loads the template document from the stored document bytes, fills its variable content with the added data and returns it. * * @return the TextDocument object corresponding to an instance of this template. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public TextDocument getInstance() throws DocumentLoadException { TextDocument document; try { document = TextDocument.loadDocument(new ByteArrayInputStream(bytes)); } catch (Exception e) { throw new DocumentLoadException(e); } fillUserFields(document, getParameters(), getLocale()); fillTables(document, getTableDataSources(), getLocale()); return document; } /** * Instantiates the template through {@link #getInstance()} and attempts to save it as a file to the given path. * * @param path the path to save the instance document to. * @throws DocumentSaveException if the document can not be written to the given path. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public void saveInstance(String path) throws DocumentSaveException, DocumentLoadException { TextDocument document = getInstance(); try { document.save(path); } catch (Exception e) { throw new DocumentSaveException(e); } document.close(); } /** * Instantiates the template through {@link #getInstance()} and attempts to save it to the given file. * * @param file the file to save the instance document to. * @throws DocumentSaveException if the document can not be written to the given file. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public void saveInstance(File file) throws DocumentLoadException, DocumentSaveException { TextDocument document = getInstance(); try { document.save(file); } catch (Exception e) { throw new DocumentSaveException(e); } document.close(); } /** * Instantiates the template through {@link #getInstance()} and attempts to save it to the given OutputStream. * * @param stream the OutputStream to save the instance document to. * @throws DocumentSaveException if the document can not be written to the given OutputStream. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public void saveInstance(OutputStream stream) throws DocumentLoadException, DocumentSaveException { TextDocument document = getInstance(); try { document.save(stream); } catch (Exception e) { throw new DocumentSaveException(e); } document.close(); } /** * Saves an instance of the template through {@link #saveInstance(OutputStream)} into a byte array and returns it. * * @return a byte array corresponding to an instance of this template. * @throws DocumentSaveException if the document can not be written to a byte array. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public byte[] getInstanceByteArray() throws DocumentLoadException, DocumentSaveException { ByteArrayOutputStream ostream = new ByteArrayOutputStream(); saveInstance(ostream); return ostream.toByteArray(); } /** * Connects to an headless OpenOffice process, sends it an instance byte array, obtained through * {@link #getInstanceByteArray()} for printing and returns a byte array with the obtained print of the instance. * * @return a byte array corresponding to a print of an instance of this template * @throws DocumentSaveException if the document can not be written to a byte array. * @throws DocumentLoadException if the document can not be created from the stored bytes. * @throws OpenOfficeConnectionException if it fails to connect to the expected headless OpenOffice process. */ public byte[] getInstancePrint() throws DocumentLoadException, DocumentSaveException, OpenOfficeConnectionException { OddjetConfiguration.ConfigurationProperties config = OddjetConfiguration.getConfiguration(); OpenOfficePrintingService service = new OpenOfficePrintingService(config.openOfficeHost(), config.openOfficePort(), config.openOfficeOutput()); return PrintUtils.print(getInstance(), service); } /** * Obtains a print of an instance through {@link #getInstancePrint()} and attempts to save it as a file to the * given path. * * @param path the path to save the instance document to. * @throws DocumentSaveException if the document can not be written to a byte array. * @throws DocumentLoadException if the document can not be created from the stored bytes. * @throws OpenOfficeConnectionException if it fails to connect to the expected headless OpenOffice process. */ public void saveInstancePrint(String path) throws DocumentLoadException, DocumentSaveException, OpenOfficeConnectionException { try { FileOutputStream ostream = new FileOutputStream(path); saveInstancePrint(ostream); } catch (FileNotFoundException e) { throw new DocumentSaveException(e); } } /** * Obtains a print of an instance through {@link #getInstancePrint()} and attempts to save it to the given file. * * @param file the file to save the instance document to. * @throws DocumentSaveException if the document can not be written to a byte array. * @throws DocumentLoadException if the document can not be created from the stored bytes. * @throws OpenOfficeConnectionException if it fails to connect to the expected headless OpenOffice process. */ public void saveInstancePrint(File file) throws DocumentLoadException, DocumentSaveException, OpenOfficeConnectionException { FileOutputStream ostream; try { ostream = new FileOutputStream(file); saveInstancePrint(ostream); } catch (FileNotFoundException e) { throw new DocumentSaveException(e); } } /** * Obtains a print of an instance through {@link #getInstancePrint()} and attempts to save it to the given * OutputStream. * * @param stream the OutputStream to save the instance document to. * @throws DocumentSaveException if the document can not be written to a byte array. * @throws DocumentLoadException if the document can not be created from the stored bytes. * @throws OpenOfficeConnectionException if it fails to connect to the expected headless OpenOffice process. */ public void saveInstancePrint(OutputStream stream) throws DocumentLoadException, DocumentSaveException, OpenOfficeConnectionException { try { stream.write(getInstancePrint()); } catch (IOException e) { throw new DocumentSaveException(e); } } /** * Returns the page count of an instance. To do this an instance must be fully generated, which is time-consuming. * * @return the page count of the template instance. * @throws DocumentLoadException if the document can not be created from the stored bytes. */ public int getInstancePageCount() throws DocumentLoadException { TextDocument document = getInstance(); try { OdfMetaDom meta = document.getMetaDom(); Node statistics = meta.getElementsByTagName("meta:document-statistic").item(0); return Integer.parseInt(statistics.getAttributes().getNamedItem("meta:page-count").getNodeValue()); } catch (Exception e) { throw new DocumentLoadException(e); } } private static void fillUserFields(TextDocument document, Map<String, Object> parameters, Locale locale) { NodeList nodes; try { nodes = document.getContentRoot().getElementsByTagName("text:user-field-decl"); } catch (Exception e) { logger.error("Failed to create the file DOM while filling the user fields."); e.printStackTrace(); return; } for (int i = 0; i < nodes.getLength(); i++) { String userFieldName = nodes.item(i).getAttributes().getNamedItem("text:name").getNodeValue(); VariableField var = document.getVariableFieldByName(userFieldName); try { Object fieldValue = resolveAttributeChain(parameters, userFieldName); var.updateField(translate(fieldValue, locale), null); } catch (AttributeChainResolutionFailureException e) { logger.error(e.getMessage()); } } } /** * Resolves a chain of attributes by getting the first attribute's value from the root object, and from it getting the next * attribute's value successively until there are no attributes in the chain returning the last object evaluated. The * attribute may be a key in a map, a public method (with 0 parameters) or field of the object, or an inaccessible field with * an accessible "get","is" or "has" method. * * @param root the root object for the chain * @param attributeChain the chain of attributes to resolve. The attribute names in the chain are expected to be separated by * dots. * @return the object that can be accessed following the provided attribute chain starting at the root object * @throws AttributeChainResolutionFailureException if attributeChain is null or if the attributes in the chain are not found * or their value is null or unaccessible */ public static Object resolveAttributeChain(Object root, String attributeChain) throws AttributeChainResolutionFailureException { return resolveAttributeChain(root, getAttributeChainComponents(attributeChain)); } // Adapted from https://github.com/mbosecke/pebble/blob/master/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java#L43 private static Object resolveAttributeChain(Object root, List<String> chainComponents) throws AttributeChainResolutionFailureException { Object result = root; for (String attributeName : chainComponents) { if (result == null) { throw new AttributeChainResolutionFailureException("Could not resolve attribute chain. Object containing '" + attributeName + "' is null."); } boolean found = false; if (!found) { try { if (result instanceof Map && ((Map<?, ?>) result).containsKey(attributeName)) { result = ((Map<?, ?>) result).get(attributeName); found = true; } } catch (ClassCastException e) { //ignores maps where keys are not strings }; } if (!found) { try { Member member = null; member = findMember(result, attributeName); if (member != null) { if (member instanceof Method) { result = ((Method) member).invoke(result); found = true; } else if (member instanceof Field) { result = ((Field) member).get(result); found = true; } } } catch (IllegalAccessException e) { throw new AttributeChainResolutionFailureException("Could not resolve attribute chain. Attribute '" + attributeName + "' is not accessible.", e); } catch (IllegalArgumentException e) { throw new AttributeChainResolutionFailureException("Could not resolve attribute chain. Method matching '" + attributeName + "' requires arguments.", e); } catch (InvocationTargetException e) { throw new AttributeChainResolutionFailureException( "Could not resolve attribute chain. Exception ocurred while evaluating the method matching '" + attributeName + "'.", e); } } if (!found) { throw new AttributeChainResolutionFailureException("No match was found for '" + attributeName + "'."); } } return result; } private static List<String> getAttributeChainComponents(String attributeChain) throws AttributeChainResolutionFailureException { if (attributeChain == null) { throw new AttributeChainResolutionFailureException("Attribute chain string representation is null."); } return new ArrayList<String>(Arrays.asList(attributeChain.split(ATTRIBUTE_ACCESS_REGEX))); } // Copied and adapted from https://github.com/mbosecke/pebble/blob/master/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java#L43 private static Member findMember(Object object, String attributeName) throws IllegalAccessException { if (attributeName.isEmpty()) { return null; } Class<?> clazz = object.getClass(); boolean found = false; Member result = null; // capitalize first letter of attribute for the following attempts String attributeCapitalized = Character.toUpperCase(attributeName.charAt(0)) + attributeName.substring(1); // check get method if (!found) { try { result = clazz.getMethod("get" + attributeCapitalized); found = true; } catch (NoSuchMethodException | SecurityException e) { } } // check is method if (!found) { try { result = clazz.getMethod("is" + attributeCapitalized); found = true; } catch (NoSuchMethodException | SecurityException e) { } } // check has method if (!found) { try { result = clazz.getMethod("has" + attributeCapitalized); found = true; } catch (NoSuchMethodException | SecurityException e) { } } // check if attribute is a public method if (!found) { try { result = clazz.getMethod(attributeName); found = true; } catch (NoSuchMethodException | SecurityException e) { } } // public field if (!found) { try { result = clazz.getField(attributeName); found = true; } catch (NoSuchFieldException | SecurityException e) { } } if (result != null) { ((AccessibleObject) result).setAccessible(true); } return result; } private static void fillTables(TextDocument document, Map<String, TableData> tableDataSources, Locale locale) { for (Table table : document.getTableList()) { TableCall tc = null; TableData td = null; try { tc = new TableCall(table.getTableName()); } catch (IllegalTableCallRepresentationException e) { logger.warn("Table name " + table.getTableName() + " does not conform to table call notation, assumed to be static table."); continue; } String tableName = tc.getTableName(); String tableSourceName = tc.getTableDataSourceName(); td = tableDataSources.get(tableSourceName); if (td == null) { logger.warn("No matching data source was found for table " + tableName + ", assumed to be static table."); continue; } TableConfiguration tp = tc.getParameters(); TableCoordinate headers = tp.getHeader(); TableCoordinate styleRCoord = tp.getStyleRelativeCoord(); ContentStructure structure = tp.getContentStructure(); int hCol = headers.getColumn(); int hRow = headers.getRow(); // Check if table has necessary cells predefined if (structure != ContentStructure.CATEGORICAL && (hRow >= table.getRowCount() || hCol >= table.getColumnCount())) { logger.error("Table dimensions of " + table.getTableName() + " do not allow the specification of the semantic data. Default category order assumed."); structure = ContentStructure.POSITIONAL; } if ((styleRCoord != null && (hRow + styleRCoord.getRow() > table.getRowCount() || hCol + styleRCoord.getColumn() > table .getColumnCount())) || (tp.getLastBorderSourceSection() == LastBorderSourceSection.BODY && (table.getRowCount() == hRow || table .getColumnCount() == hCol))) { logger.error("Table dimensions of " + table.getTableName() + " are not suficient to specify the table cell format. Default cell style will be used."); styleRCoord = null; } // Collect all the styles of the predefined style cells before adding any new cells. // This is only necessary due to a quirk in the simpleAPI where creating a new column/row changes the style of the cell // in the previous column/row. Map<String, String> cellStyles = collectCellStyles(table, hCol, hRow, styleRCoord); Border lastBorder = collectLastBorder(table, hCol, hRow, tp.getLastBorderSourceSection(), tp.getLastBorderSourceType()); // Get the positional version of the data ( using the category order in the template table in the semantic case ) List<List<Object>> data = null; if (structure == ContentStructure.CATEGORICAL) { List<String> categoryOrder = null; categoryOrder = getCategoryOrder(table, headers, tp.getContentDirection()); data = td.getData(categoryOrder); } else { data = td.getData(); } boolean isDataEmpty = true; int depth = 0; if (data != null) { for (List<Object> cat : data) { depth = cat.size() > depth ? cat.size() : depth; } isDataEmpty = depth == 0; } Fields.createUserVariableField(document, tableSourceName + "_isEmpty", "" + isDataEmpty); if (isDataEmpty) { logger.warn("Data source for table '" + table.getTableName() + "' is empty, assumed to be a static table."); continue; } Fields.createUserVariableField(document, tableName + "_dataSize", "" + data.size()); Fields.createUserVariableField(document, tableName + "_dataDepth", "" + depth); int X, Y, i, j, startX, startY, limitX, limitY, tableDimX, tableSpaceX, tableDimY, tableSpaceY, nData = 0; if (tp.getContentDirection() == ContentDirection.VERTICAL) { startX = hCol; startY = hRow; tableDimX = table.getRowByIndex(hRow).getCellCount(); tableDimY = table.getColumnByIndex(hCol).getCellCount(); } else { startX = hRow; startY = hCol; tableDimX = table.getColumnByIndex(hCol).getCellCount(); tableDimY = table.getRowByIndex(hRow).getCellCount(); } tableSpaceX = startY > 0 ? tableDimX - startX : -1; limitX = data.size(); if (tableSpaceX > 0) { if (tableSpaceX < limitX) { limitX = tableSpaceX; logger.warn("Too many data categories for the allocated table space in table '" + table.getTableName() + "'. The remaining categories beyond table limits will be ignored."); } else if (tableSpaceX > limitX) { logger.warn("Too few data categories for the allocated table space in table '" + table.getTableName() + "'. The remaining space will be empty."); } } for (X = startX, i = 0; i < limitX; i++, X++) { List<Object> dataCategory = data.get(i); tableSpaceY = startX > 0 ? tableDimY - startY : -1; boolean overflowReported = false; limitY = dataCategory != null ? dataCategory.size() : 0; if (tableSpaceY > 0) { if (tableSpaceY < limitY) { limitY = tableSpaceY; logger.warn("Data category nr." + X + " has more data than the allocated table space allows for in table '" + table.getTableName() + "'. Remaining data will be ignored."); overflowReported = true; } } for (Y = startY, j = 0; j < limitY; j++, Y++) { Cell cell = tp.getContentDirection() == ContentDirection.VERTICAL ? table.getCellByPosition(X, Y) : table .getCellByPosition(Y, X); switch (tp.getFillBehavior()) { //FIXME Fall through here allows cleaner code but it's a little less efficient. case STEP: // If there is a paragraph with content then don't do anything, else fall through if (cell.getParagraphByIndex(0, true) != null) { break; } case SKIP: // If there is a paragraph with content then just rollback the data to be reused and recheck for data overflows, else fall through if (cell.getParagraphByIndex(0, true) != null) { j--; if (tableSpaceY > 0) { if (tableDimY - Y < limitY - j) { limitY = tableDimY - Y; if (!overflowReported) { logger.warn("Data category nr." + X + " has more data than the allocated table space allows for in table '" + table.getTableName() + "'. Remaining data will be ignored."); overflowReported = true; } } } break; } case WRITE: nData++; switch (tp.getWriteBehavior()) { case APPEND: // Get the last paragraph and if it exists add the data's text to it, else fall through Paragraph lastParagraph = cell.getParagraphByReverseIndex(0, false); if (lastParagraph != null) { lastParagraph.getOdfElement().setTextContent( lastParagraph.getTextContent() + translate(dataCategory.get(j), locale)); break; } case PREPEND: // Get the first paragraph and if it exists add the data's text to it, else fall through Paragraph firstParagraph = cell.getParagraphByIndex(0, false); if (firstParagraph != null) { firstParagraph.getOdfElement().setTextContent( translate(dataCategory.get(j) + firstParagraph.getTextContent(), locale)); break; } case OVERWRITE: cell.removeTextContent(); cell.addParagraph(translate(dataCategory.get(j), locale)); break; default: logger.error("Atempted to use unimplemented Write Behavior: " + tp.getWriteBehavior().name() + "."); } break; default: logger.error("Atempted to use unimplemented Fill Behavior: " + tp.getFillBehavior().name() + "."); } } } // Create table relative automatic fields with table statistics Fields.createUserVariableField(document, tableName + "_nRow", "" + table.getRowCount()); Fields.createUserVariableField(document, tableName + "_nCol", "" + table.getColumnCount()); Fields.createUserVariableField(document, tableName + "_nData", "" + nData); // Apply the correct formatting to each cell in the table if (cellStyles != null) { int sCol = styleRCoord.getColumn(); int sRow = styleRCoord.getRow(); for (i = hCol; i < table.getColumnCount(); i++) { for (j = hRow; j < table.getRowCount(); j++) { Cell cell = table.getCellByPosition(i, j); TableCoordinate styleCellCoord; if (sCol == 0) { // vertical styleCellCoord = new TableCoordinate(i, j % sRow + hRow); } else if (sRow == 0) { // horizontal styleCellCoord = new TableCoordinate(i % sCol + hCol, j); } else { // periodic int jumps = Math.min((i - hCol) / sCol, (j - hRow) / sRow); styleCellCoord = new TableCoordinate(i - jumps * sCol, j - jumps * sRow); } // Copy style cell style properties cell.setCellStyleName(cellStyles.get(styleCellCoord.toString())); // Copy paragraph style Cell styleCell = table.getCellByPosition(styleCellCoord.getColumn(), styleCellCoord.getRow()); Iterator<Paragraph> pit = cell.getParagraphIterator(); Iterator<Paragraph> spit = styleCell.getParagraphIterator(); while (pit.hasNext() && spit.hasNext()) { //pit.next().setStyleName(spit.next().getStyleName()); //FIXME Not working, figure out why... copyStyle(spit.next().getOdfElement(), pit.next().getOdfElement()); } } } } //Change the last border of the table if (lastBorder != null) { CellBordersType lastBorderType; CellRange lastCells = null; if (tp.getContentDirection() == ContentDirection.VERTICAL) { lastBorderType = CellBordersType.BOTTOM; lastCells = table.getCellRangeByPosition(headers.getColumn(), table.getRowCount() - 1, table.getColumnCount() - 1, table.getRowCount() - 1); } else { lastBorderType = CellBordersType.LEFT; lastCells = table.getCellRangeByPosition(table.getColumnCount() - 1, headers.getRow(), table.getColumnCount() - 1, table.getRowCount() - 1); } for (i = 0; i < lastCells.getColumnNumber(); i++) { for (j = 0; j < lastCells.getRowNumber(); j++) { lastCells.getCellByPosition(i, j).setBorders(lastBorderType, lastBorder); } } } } } // XXX This breaks if the cells contain any "none" border attribute. private static Border collectLastBorder(Table table, int hCol, int hRow, LastBorderSourceSection lastBorderOrigin, CellBordersType lastBorderOriginType) { Border border = null; if (lastBorderOrigin != null) { border = Border.NONE; switch (lastBorderOrigin) { case HEADER: switch (lastBorderOriginType) { case LEFT: case TOP: border = table.getCellByPosition(0, 0).getBorder(lastBorderOriginType); break; case RIGHT: case BOTTOM: if (hCol != 0 && hRow != 0) { border = null; } else { border = table.getCellByPosition((hCol != 0 ? hCol : table.getColumnCount()) - 1, (hRow != 0 ? hRow : table.getRowCount()) - 1).getBorder(lastBorderOriginType); } break; default: break; } break; case BODY: switch (lastBorderOriginType) { case LEFT: case TOP: border = table.getCellByPosition(hCol, hRow).getBorder(lastBorderOriginType); break; case RIGHT: case BOTTOM: border = table.getCellByPosition(table.getColumnCount() - 1, table.getRowCount() - 1).getBorder( lastBorderOriginType); break; default: break; } break; default: break; } } return border; } // XXX This may break if the element contains any "none" border attribute. private static boolean copyStyle(OdfStylableElement from, OdfStylableElement to) { if (to.getStyleFamily().equals(from.getStyleFamily())) { for (OdfStyleProperty prop : from.getStyleFamily().getProperties()) { String value = from.getProperty(prop); if (value != null) { to.setProperty(prop, value); } } return true; }; return false; } private static Map<String, String> collectCellStyles(Table table, int hCol, int hRow, TableCoordinate styleRCoord) { Map<String, String> cellStyles = null; if (styleRCoord != null) { cellStyles = new HashMap<>(); for (int i = hCol; i < table.getColumnCount(); i++) { int limit = i > styleRCoord.getColumn() ? styleRCoord.getRow() + hRow : table.getRowCount(); for (int j = hCol; j < limit; j++) { cellStyles.put(new TableCoordinate(i, j).toString(), table.getCellByPosition(i, j).getStyleName()); } } } return cellStyles; } private static List<String> getCategoryOrder(Table table, TableCoordinate headers, ContentDirection fdir) { List<String> categoryOrder = new ArrayList<String>(); CellRange categoryRange = null; if (fdir == ContentDirection.VERTICAL) { categoryRange = table.getCellRangeByPosition(headers.getColumn(), headers.getRow(), table.getColumnCount() - 1, headers.getRow()); } else { categoryRange = table.getCellRangeByPosition(headers.getColumn(), headers.getRow(), headers.getColumn(), table.getRowCount() - 1); } for (int i = 0; i < categoryRange.getColumnNumber(); i++) { for (int j = 0; j < categoryRange.getRowNumber(); j++) { Cell cell = categoryRange.getCellByPosition(i, j); Paragraph categoryParagraph = cell.getParagraphByIndex(0, false); String category = null; if (categoryParagraph == null || (category = categoryParagraph.getTextContent().trim()).isEmpty()) { logger.warn("Data category missing at (" + i + "," + j + ") in table '" + table.getTableName() + "'."); categoryOrder.add(null); } else { cell.removeParagraph(categoryParagraph); categoryOrder.add(category); }; } } return categoryOrder; } private static String translate(Object object, Locale locale) { try { Method m = object.getClass().getMethod("getContent", Locale.class); Object content = m.invoke(object, locale); if (content == null) { try { m = object.getClass().getMethod("getContent"); content = m.invoke(object, locale); } catch (Exception e) { } } return content != null ? content.toString() : ""; } catch (Exception e) { } return object != null ? object.toString() : ""; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }