None: Only files without password protection can be updated.
\n
Credentials: Use a password set via workflow credentials.
\n
Password: Specify a password.
\n
",
+ "default" : "NONE"
+ }
+ },
+ "default" : {
+ "type" : "NONE",
+ "credentials" : "",
+ "password" : {
+ "isHiddenPassword" : false,
+ "isHiddenSecondFactor" : false,
+ "username" : ""
+ }
+ }
+ },
+ "encryptionValidationMessage" : {
+ "type" : "object"
+ },
+ "evaluateFormulas" : {
+ "type" : "boolean",
+ "title" : "Evaluate formulas (leave unchecked if uncertain, see help for details)",
+ "description" : "If checked, all formulas in the file will be evaluated after the sheet has been written. This is useful if other sheets in the file refer to the data just written and their content needs updating. This option is only relevant if the append option is selected. This can cause errors when there are functions that are not implemented by the underlying Apache POI library. Note: For xlsx files, evaluation requires significantly more memory as the whole file needs to be kept in memory (xls files are anyway loaded completely into memory).",
+ "default" : false
+ },
+ "excelFormat" : {
+ "oneOf" : [ {
+ "const" : "XLSX",
+ "title" : "XLSX"
+ }, {
+ "const" : "XLS",
+ "title" : "XLS"
+ } ],
+ "title" : "Excel format",
+ "description" : "Select the Excel file format to write.\n
\n
XLSX: The Office Open XML format is the file format used by default from Excel 2007 onwards. The maximum number of columns and rows held by a spreadsheet of this format is 16384 and 1048576 respectively.
\n
XLS: This is the file format which was used by default up until Excel 2003. The maximum number of columns and rows held by a spreadsheet of this format is 256 and 65536 respectively.
\n
",
+ "default" : "XLSX"
+ },
+ "ifSheetExists" : {
+ "oneOf" : [ {
+ "const" : "FAIL",
+ "title" : "Fail"
+ }, {
+ "const" : "OVERWRITE",
+ "title" : "Overwrite"
+ }, {
+ "const" : "APPEND",
+ "title" : "Append"
+ } ],
+ "title" : "If sheet exists",
+ "description" : "Specify the behavior of the node in case a sheet with the entered name already exists. (This option is only relevant if the file append option is selected.)\n
\n
Fail: Will issue an error during the node's execution (to prevent unintentional overwrite).
\n
Overwrite: Will replace any existing sheet.
\n
Append: Will append the input tables after the last row of the sheets. Note: the last row is chosen according to the rows which already exist in the sheet. A row may appear empty but still exist because it or one of its cells contains styling information or was not removed by Excel after the user cleared it.
None: Only files without password protection can be updated.
\n
Credentials: Use a password set via workflow credentials.
\n
Password: Specify a password.
\n
",
+ "default" : "NONE"
+ }
+ },
+ "default" : {
+ "type" : "NONE",
+ "credentials" : "",
+ "password" : {
+ "isHiddenPassword" : false,
+ "isHiddenSecondFactor" : false,
+ "username" : ""
+ }
+ }
+ },
+ "encryptionValidationMessage" : {
+ "type" : "object"
+ },
+ "evaluateFormulas" : {
+ "type" : "boolean",
+ "title" : "Evaluate formulas (leave unchecked if uncertain, see help for details)",
+ "description" : "If checked, all formulas in the file will be evaluated after the sheet has been written. This is useful if other sheets in the file refer to the data just written and their content needs updating. This option is only relevant if the append option is selected. This can cause errors when there are functions that are not implemented by the underlying Apache POI library. Note: For xlsx files, evaluation requires significantly more memory as the whole file needs to be kept in memory (xls files are anyway loaded completely into memory).",
+ "default" : false
+ },
+ "excelFormat" : {
+ "oneOf" : [ {
+ "const" : "XLSX",
+ "title" : "XLSX"
+ }, {
+ "const" : "XLS",
+ "title" : "XLS"
+ } ],
+ "title" : "Excel format",
+ "description" : "Select the Excel file format to write.\n
\n
XLSX: The Office Open XML format is the file format used by default from Excel 2007 onwards. The maximum number of columns and rows held by a spreadsheet of this format is 16384 and 1048576 respectively.
\n
XLS: This is the file format which was used by default up until Excel 2003. The maximum number of columns and rows held by a spreadsheet of this format is 256 and 65536 respectively.
\n
",
+ "default" : "XLSX"
+ },
+ "ifSheetExists" : {
+ "oneOf" : [ {
+ "const" : "FAIL",
+ "title" : "Fail"
+ }, {
+ "const" : "OVERWRITE",
+ "title" : "Overwrite"
+ }, {
+ "const" : "APPEND",
+ "title" : "Append"
+ } ],
+ "title" : "If sheet exists",
+ "description" : "Specify the behavior of the node in case a sheet with the entered name already exists. (This option is only relevant if the file append option is selected.)\n
\n
Fail: Will issue an error during the node's execution (to prevent unintentional overwrite).
\n
Overwrite: Will replace any existing sheet.
\n
Append: Will append the input tables after the last row of the sheets. Note: the last row is chosen according to the rows which already exist in the sheet. A row may appear empty but still exist because it or one of its cells contains styling information or was not removed by Excel after the user cleared it.
Fail: Will issue an error during the node's execution (to prevent unintentional overwrite).
Overwrite: Will replace any existing file.
Append: Will append the input data to an existing file.
",
+ "default" : "overwrite"
+ }
+ },
+ "default" : {
+ "file" : {
+ "path" : {
+ "fsCategory" : "LOCAL",
+ "path" : "",
+ "timeout" : 10000,
+ "context" : {
+ "fsToString" : "(LOCAL, )"
+ }
+ }
+ },
+ "createMissingFolders" : false,
+ "overwritePolicy" : "overwrite"
+ }
+ },
+ "paperSize" : {
+ "oneOf" : [ {
+ "const" : "A4_PAPERSIZE",
+ "title" : "A4 - 210x297 mm"
+ }, {
+ "const" : "A5_PAPERSIZE",
+ "title" : "A5 - 148x210 mm"
+ }, {
+ "const" : "ENVELOPE_10_PAPERSIZE",
+ "title" : "US Envelope #10 4 1/8 x 9 1/2"
+ }, {
+ "const" : "ENVELOPE_CS_PAPERSIZE",
+ "title" : "Envelope C5 162x229 mm"
+ }, {
+ "const" : "ENVELOPE_DL_PAPERSIZE",
+ "title" : "Envelope DL 110x220 mm"
+ }, {
+ "const" : "ENVELOPE_MONARCH_PAPERSIZE",
+ "title" : "Envelope Monarch 98.4×190.5 mm"
+ }, {
+ "const" : "EXECUTIVE_PAPERSIZE",
+ "title" : "US Executive 7 1/4 x 10 1/2 in"
+ }, {
+ "const" : "LEGAL_PAPERSIZE",
+ "title" : "US Legal 8 1/2 x 14 in"
+ }, {
+ "const" : "LETTER_PAPERSIZE",
+ "title" : "US Letter 8 1/2 x 11 in"
+ } ],
+ "title" : "Paper size",
+ "description" : "Sets the paper size in the print setup.",
+ "default" : "A4_PAPERSIZE"
+ },
+ "sheetNameValidationMessage" : {
+ "type" : "object"
+ },
+ "sheetNames" : {
+ "type" : "array",
+ "items" : {
+ "type" : "object",
+ "properties" : {
+ "sheetName" : {
+ "type" : "string",
+ "title" : "Sheet name",
+ "description" : "Name of the spreadsheets that will be created. The dropdown can be used to select a sheet name which already exists in the Excel file or a custom name can be entered. If \"If sheet exists\" isn't set to append, each sheet name must be unique. The node appends the tables in the order they are connected.",
+ "default" : "Sheet1"
+ }
+ }
+ },
+ "title" : "Sheets",
+ "default" : [ ]
+ },
+ "skipColumnHeaderOnAppend" : {
+ "type" : "boolean",
+ "title" : "Don't write column headers if sheet exists",
+ "description" : "Only write the column headers if a sheet is newly created or replaced. This option is convenient if you have written data with the same specification to an existing sheet before and want to append new rows to it.",
+ "default" : true
+ },
+ "writeColumnHeaders" : {
+ "type" : "boolean",
+ "title" : "Write column headers",
+ "description" : "If checked, the column names are written out in the first row of the spreadsheet.",
+ "default" : true
+ },
+ "writeRowKey" : {
+ "type" : "boolean",
+ "title" : "Write row key",
+ "description" : "If checked, the row IDs are added to the output, in the first column of the spreadsheet.",
+ "default" : false
+ }
+ }
+ }
+ }
+ },
+ "ui_schema" : {
+ "elements" : [ {
+ "label" : "Output",
+ "type" : "Section",
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/excelFormat",
+ "options" : {
+ "format" : "valueSwitch"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/outputFile/properties/file",
+ "options" : {
+ "isLocal" : true,
+ "isWriter" : true,
+ "spaceFSOptions" : {
+ "mountId" : "Local space"
+ },
+ "format" : "fileChooser"
+ },
+ "providedOptions" : [ "fileExtension" ]
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/outputFile/properties/createMissingFolders",
+ "options" : {
+ "format" : "checkbox"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/outputFile/properties/overwritePolicy",
+ "options" : {
+ "format" : "valueSwitch"
+ },
+ "providedOptions" : [ "possibleValues" ]
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/encryption/properties/type",
+ "options" : {
+ "format" : "valueSwitch"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/encryption/properties/credentials",
+ "options" : {
+ "format" : "dropDown"
+ },
+ "providedOptions" : [ "possibleValues" ],
+ "rule" : {
+ "effect" : "SHOW",
+ "condition" : {
+ "scope" : "#/properties/model/properties/encryption/properties/type",
+ "schema" : {
+ "oneOf" : [ {
+ "const" : "CREDENTIALS"
+ } ]
+ }
+ }
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/encryption/properties/password",
+ "options" : {
+ "hasUsername" : false,
+ "passwordLabel" : "Password",
+ "format" : "credentials"
+ },
+ "rule" : {
+ "effect" : "SHOW",
+ "condition" : {
+ "scope" : "#/properties/model/properties/encryption/properties/type",
+ "schema" : {
+ "oneOf" : [ {
+ "const" : "PWD"
+ } ]
+ }
+ }
+ }
+ }, {
+ "type" : "Control",
+ "id" : "#/properties/model/properties/encryptionValidationMessage",
+ "options" : {
+ "format" : "textMessage"
+ },
+ "providedOptions" : [ "message" ],
+ "rule" : {
+ "effect" : "SHOW",
+ "condition" : {
+ "scope" : "#/properties/model/properties/outputFile",
+ "schema" : {
+ "properties" : {
+ "overwritePolicy" : {
+ "oneOf" : [ {
+ "const" : "append"
+ } ]
+ }
+ },
+ "required" : [ "overwritePolicy" ]
+ }
+ }
+ }
+ } ]
+ }, {
+ "label" : "Sheets",
+ "type" : "Section",
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/sheetNames",
+ "options" : {
+ "detail" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/sheetName",
+ "options" : {
+ "format" : "dropDown",
+ "allowNewValue" : true
+ },
+ "providedOptions" : [ "possibleValues" ]
+ } ],
+ "elementLayout" : "HORIZONTAL_SINGLE_LINE",
+ "hasFixedSize" : true
+ },
+ "providedOptions" : [ "elementDefaultValue" ]
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/ifSheetExists",
+ "options" : {
+ "format" : "valueSwitch"
+ },
+ "rule" : {
+ "effect" : "SHOW",
+ "condition" : {
+ "scope" : "#/properties/model/properties/outputFile",
+ "schema" : {
+ "properties" : {
+ "overwritePolicy" : {
+ "oneOf" : [ {
+ "const" : "append"
+ } ]
+ }
+ },
+ "required" : [ "overwritePolicy" ]
+ }
+ }
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/concatenateSheetsWithSameName",
+ "options" : {
+ "format" : "checkbox"
+ },
+ "rule" : {
+ "effect" : "HIDE",
+ "condition" : {
+ "scope" : "#/properties/model/properties/outputFile",
+ "schema" : {
+ "properties" : {
+ "overwritePolicy" : {
+ "oneOf" : [ {
+ "const" : "append"
+ } ]
+ }
+ },
+ "required" : [ "overwritePolicy" ]
+ }
+ }
+ }
+ }, {
+ "type" : "Control",
+ "id" : "#/properties/model/properties/sheetNameValidationMessage",
+ "options" : {
+ "format" : "textMessage"
+ },
+ "providedOptions" : [ "message" ]
+ } ]
+ }, {
+ "label" : "Headers / Keys",
+ "type" : "Section",
+ "options" : {
+ "isAdvanced" : true
+ },
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/writeRowKey",
+ "options" : {
+ "format" : "checkbox"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/writeColumnHeaders",
+ "options" : {
+ "format" : "checkbox"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/skipColumnHeaderOnAppend",
+ "options" : {
+ "format" : "checkbox"
+ }
+ } ]
+ }, {
+ "label" : "Values",
+ "type" : "Section",
+ "options" : {
+ "isAdvanced" : true
+ },
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/missingValuePattern",
+ "options" : {
+ "placeholder" : "Replacement value",
+ "hideOnNull" : true,
+ "default" : ""
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/evaluateFormulas",
+ "options" : {
+ "format" : "checkbox"
+ }
+ } ]
+ }, {
+ "label" : "Layout",
+ "type" : "Section",
+ "options" : {
+ "isAdvanced" : true
+ },
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/autosizeColumns",
+ "options" : {
+ "format" : "checkbox"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/orientation",
+ "options" : {
+ "format" : "valueSwitch"
+ }
+ }, {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/paperSize"
+ } ]
+ }, {
+ "label" : "Interaction",
+ "type" : "Section",
+ "options" : {
+ "isAdvanced" : true
+ },
+ "elements" : [ {
+ "type" : "Control",
+ "scope" : "#/properties/model/properties/openOutputFileAfterExecution",
+ "options" : {
+ "format" : "checkbox"
+ }
+ } ]
+ } ]
+ },
+ "persist" : {
+ "type" : "object",
+ "properties" : {
+ "model" : {
+ "type" : "object",
+ "properties" : {
+ "excelFormat" : {
+ "configKey" : "excel_format"
+ },
+ "outputFile" : {
+ "type" : "object",
+ "properties" : {
+ "file" : {
+ "configPaths" : [ [ "path" ] ]
+ },
+ "createMissingFolders" : {
+ "configKey" : "create_missing_folders"
+ },
+ "overwritePolicy" : {
+ "configKey" : "if_path_exists"
+ }
+ },
+ "configKey" : "file_selection"
+ },
+ "encryption" : {
+ "type" : "object",
+ "properties" : {
+ "type" : {
+ "configKey" : "selectedType"
+ },
+ "credentials" : {
+ "configKey" : "credentials"
+ },
+ "password" : {
+ "configPaths" : [ ]
+ }
+ },
+ "configKey" : "authentication_method"
+ },
+ "encryptionValidationMessage" : { },
+ "sheetNames" : {
+ "configPaths" : [ [ "sheet_names" ] ]
+ },
+ "ifSheetExists" : {
+ "configKey" : "if_sheet_exists"
+ },
+ "concatenateSheetsWithSameName" : { },
+ "sheetNameValidationMessage" : { },
+ "writeRowKey" : {
+ "configKey" : "write_row_key"
+ },
+ "writeColumnHeaders" : {
+ "configKey" : "write_column_header"
+ },
+ "skipColumnHeaderOnAppend" : {
+ "configKey" : "skip_column_header_on_append"
+ },
+ "missingValuePattern" : {
+ "configPaths" : [ [ "replace_missings" ], [ "missing_value_pattern" ] ]
+ },
+ "evaluateFormulas" : {
+ "configKey" : "evaluate_formulas"
+ },
+ "autosizeColumns" : {
+ "configKey" : "autosize_columns"
+ },
+ "orientation" : {
+ "configKey" : "layout"
+ },
+ "paperSize" : {
+ "configKey" : "paper_size"
+ },
+ "openOutputFileAfterExecution" : {
+ "configKey" : "open_file_after_exec_Internals"
+ }
+ }
+ }
+ }
+ },
+ "initialUpdates" : [ {
+ "scope" : "#/properties/model/properties/concatenateSheetsWithSameName",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : true
+ } ]
+ }, {
+ "scope" : "#/properties/model/properties/encryption/properties/credentials",
+ "providedOptionName" : "possibleValues",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : [ ]
+ } ]
+ }, {
+ "scope" : "#/properties/model/properties/outputFile/properties/file",
+ "providedOptionName" : "fileExtension",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : "xls"
+ } ]
+ }, {
+ "scope" : "#/properties/model/properties/outputFile/properties/overwritePolicy",
+ "providedOptionName" : "possibleValues",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : [ {
+ "id" : "fail",
+ "text" : "Fail"
+ }, {
+ "id" : "overwrite",
+ "text" : "Overwrite"
+ }, {
+ "id" : "append",
+ "text" : "Append"
+ } ]
+ } ]
+ }, {
+ "scope" : "#/properties/model/properties/sheetNames",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : [ ]
+ } ]
+ }, {
+ "id" : "#/properties/model/properties/encryptionValidationMessage",
+ "providedOptionName" : "message",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : null
+ } ]
+ }, {
+ "id" : "#/properties/model/properties/sheetNameValidationMessage",
+ "providedOptionName" : "message",
+ "values" : [ {
+ "indices" : [ ],
+ "value" : null
+ } ]
+ } ],
+ "globalUpdates" : [ {
+ "trigger" : {
+ "id" : "after-open-dialog"
+ },
+ "triggerInitially" : true,
+ "dependencies" : [ "#/properties/model/properties/encryption", "#/properties/model/properties/outputFile/properties/file" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/concatenateSheetsWithSameName"
+ },
+ "dependencies" : [ "#/properties/model/properties/concatenateSheetsWithSameName", "#/properties/model/properties/ifSheetExists" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/encryption"
+ },
+ "dependencies" : [ "#/properties/model/properties/encryption", "#/properties/model/properties/outputFile/properties/file" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/excelFormat"
+ },
+ "dependencies" : [ "#/properties/model/properties/excelFormat", "#/properties/model/properties/outputFile/properties/file" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/ifSheetExists"
+ },
+ "dependencies" : [ "#/properties/model/properties/ifSheetExists", "#/properties/model/properties/outputFile/properties/overwritePolicy", "#/properties/model/properties/sheetNames" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/outputFile"
+ },
+ "dependencies" : [ "#/properties/model/properties/ifSheetExists" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/outputFile/properties/file"
+ },
+ "dependencies" : [ "#/properties/model/properties/encryption", "#/properties/model/properties/outputFile/properties/file" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/outputFile/properties/overwritePolicy"
+ },
+ "dependencies" : [ "#/properties/model/properties/ifSheetExists", "#/properties/model/properties/outputFile/properties/overwritePolicy", "#/properties/model/properties/sheetNames" ]
+ }, {
+ "trigger" : {
+ "scope" : "#/properties/model/properties/sheetNames"
+ },
+ "dependencies" : [ "#/properties/model/properties/ifSheetExists", "#/properties/model/properties/outputFile/properties/overwritePolicy", "#/properties/model/properties/sheetNames" ]
+ } ]
+}
\ No newline at end of file
diff --git a/org.knime.ext.poi3.tests/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParametersTest.java b/org.knime.ext.poi3.tests/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParametersTest.java
new file mode 100644
index 00000000..975c74a3
--- /dev/null
+++ b/org.knime.ext.poi3.tests/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParametersTest.java
@@ -0,0 +1,215 @@
+/*
+ * ------------------------------------------------------------------------
+ *
+ * Copyright by KNIME AG, Zurich, Switzerland
+ * Website: http://www.knime.com; Email: contact@knime.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, Version 3, as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs.
+ * Hence, KNIME and ECLIPSE are both independent programs and are not
+ * derived from each other. Should, however, the interpretation of the
+ * GNU GPL Version 3 ("License") under any applicable laws result in
+ * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants
+ * you the additional permission to use and propagate KNIME together with
+ * ECLIPSE with only the license terms in place for ECLIPSE applying to
+ * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the
+ * license terms of ECLIPSE themselves allow for the respective use and
+ * propagation of ECLIPSE together with KNIME.
+ *
+ * Additional permission relating to nodes for KNIME that extend the Node
+ * Extension (and in particular that are based on subclasses of NodeModel,
+ * NodeDialog, and NodeView) and that only interoperate with KNIME through
+ * standard APIs ("Nodes"):
+ * Nodes are deemed to be separate and independent programs and to not be
+ * covered works. Notwithstanding anything to the contrary in the
+ * License, the License does not apply to Nodes, you are not required to
+ * license Nodes under the License, and you are granted a license to
+ * prepare and propagate Nodes, in each case even if such Nodes are
+ * propagated with or for interoperation with KNIME. The owner of a Node
+ * may freely choose the license terms applicable to such Node, including
+ * when such Node is propagated with or for interoperation with KNIME.
+ * ---------------------------------------------------------------------
+ *
+ * History
+ * Dec 15, 2025: created
+ */
+package org.knime.ext.poi3.node.io.filehandling.excel.writer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import org.eclipse.core.runtime.FileLocator;
+import org.junit.jupiter.api.Test;
+import org.knime.core.data.DataColumnSpecCreator;
+import org.knime.core.data.DataTableSpec;
+import org.knime.core.data.def.StringCell;
+import org.knime.core.node.InvalidSettingsException;
+import org.knime.core.node.NodeSettings;
+import org.knime.core.node.port.PortObjectSpec;
+import org.knime.core.util.FileUtil;
+import org.knime.core.webui.node.dialog.SettingsType;
+import org.knime.core.webui.node.dialog.defaultdialog.NodeParametersUtil;
+import org.knime.core.webui.node.dialog.defaultdialog.internal.file.FileSelection;
+import org.knime.ext.poi3.Fixtures;
+import org.knime.ext.poi3.node.io.filehandling.excel.ExcelEncryptionSettings;
+import org.knime.filehandling.core.connections.FSCategory;
+import org.knime.filehandling.core.connections.FSLocation;
+import org.knime.node.parameters.persistence.legacy.LegacyFileWriterWithOverwritePolicyOptions;
+import org.knime.node.parameters.widget.choices.StringChoice;
+import org.knime.node.parameters.widget.credentials.Credentials;
+import org.knime.testing.node.dialog.DefaultNodeSettingsSnapshotTest;
+import org.knime.testing.node.dialog.SnapshotTestConfiguration;
+import org.knime.testing.node.dialog.updates.DialogUpdateSimulator;
+
+/**
+ * Snapshot test for {@link ExcelTableWriterNodeParameters}.
+ *
+ * @author Thomas Reifenberger, TNG Technology Consulting GmbH
+ * @author AI Migration Pipeline
+ */
+@SuppressWarnings("restriction")
+final class ExcelTableWriterNodeParametersTest extends DefaultNodeSettingsSnapshotTest {
+
+ ExcelTableWriterNodeParametersTest() {
+ super(getConfig());
+ }
+
+ private static SnapshotTestConfiguration getConfig() {
+ return SnapshotTestConfiguration.builder() //
+ .withInputPortObjectSpecs(createInputPortSpecs()) //
+ .testJsonFormsForModel(ExcelTableWriterNodeParameters.class) //
+ .testJsonFormsWithInstance(SettingsType.MODEL, () -> readSettings()) //
+ .testNodeSettingsStructure(() -> readSettings()) //
+ .build();
+ }
+
+ private static ExcelTableWriterNodeParameters readSettings() {
+ try {
+ var path = getSnapshotPath(ExcelTableWriterNodeParameters.class).getParent().resolve("node_settings")
+ .resolve("ExcelTableWriterNodeParameters.xml");
+ try (var fis = new FileInputStream(path.toFile())) {
+ var nodeSettings = NodeSettings.loadFromXML(fis);
+ return NodeParametersUtil.loadSettings(nodeSettings.getNodeSettings(SettingsType.MODEL.getConfigKey()),
+ ExcelTableWriterNodeParameters.class);
+ }
+ } catch (IOException | InvalidSettingsException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static PortObjectSpec[] createInputPortSpecs() {
+ return new PortObjectSpec[]{createDefaultTestTableSpec(), createDefaultTestTableSpec(),
+ createDefaultTestTableSpec()};
+ }
+
+ private static DataTableSpec createDefaultTestTableSpec() {
+ return new DataTableSpec(new DataColumnSpecCreator("Column1", StringCell.TYPE).createSpec(),
+ new DataColumnSpecCreator("Column2", StringCell.TYPE).createSpec());
+ }
+
+ @Test
+ @SuppressWarnings("static-method")
+ void testExtensionUpdateOnExcelFormatChange() {
+ // given
+ var settings = new ExcelTableWriterNodeParameters();
+ var outputFile = new FSLocation(FSCategory.LOCAL, "workbook.xlsx");
+ var outputFileSelection = new FileSelection(outputFile);
+ settings.m_outputFile = new LegacyFileWriterWithOverwritePolicyOptions(outputFileSelection, true,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.overwrite);
+ var simulator = new DialogUpdateSimulator(settings, null);
+
+ // when
+ settings.m_excelFormat = ExcelTableWriterNodeParameters.ExcelFormat.XLS;
+ var result = simulator.simulateValueChange("excelFormat");
+
+ // then
+ assertThat(result.getValueUpdateAt("outputFile", "file"))
+ .isEqualTo(new FileSelection(new FSLocation(FSCategory.LOCAL, "workbook.xls")));
+ assertThat(result.getUiStateUpdateAt(List.of("outputFile", "file"), "fileExtension")).isEqualTo("xls");
+ }
+
+ @Test
+ @SuppressWarnings("static-method")
+ void testUnknownExtensionIsNotUpdatedOnExcelFormatChange() {
+ // given
+ var settings = new ExcelTableWriterNodeParameters();
+ var outputFile = new FSLocation(FSCategory.LOCAL, "workbook.unknown");
+ var outputFileSelection = new FileSelection(outputFile);
+ settings.m_outputFile = new LegacyFileWriterWithOverwritePolicyOptions(outputFileSelection, true,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.overwrite);
+ var simulator = new DialogUpdateSimulator(settings, null);
+
+ // when
+ settings.m_excelFormat = ExcelTableWriterNodeParameters.ExcelFormat.XLS;
+ var result = simulator.simulateValueChange("excelFormat");
+
+ // then
+ assertThat(result.hasNoValueUpdateAt("outputFile", "file")).isTrue();
+ assertThat(result.getUiStateUpdateAt(List.of("outputFile", "file"), "fileExtension")).isEqualTo("xls");
+ }
+
+ @Test
+ @SuppressWarnings("static-method")
+ void testSheetNamesAreLoadedFromUnencryptedExcelFile() throws IOException, URISyntaxException {
+ // given
+ var settings = new ExcelTableWriterNodeParameters();
+ var outputFile = new FSLocation(FSCategory.LOCAL, getTestFilePath(Fixtures.XLSX));
+ var outputFileSelection = new FileSelection(outputFile);
+ settings.m_outputFile = new LegacyFileWriterWithOverwritePolicyOptions(outputFileSelection, true,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.overwrite);
+ var simulator = new DialogUpdateSimulator(settings, null);
+
+ // when
+ var result = simulator.simulateValueChange("outputFile", "file");
+
+ // then
+ assertThat(
+ result.getUiStateUpdateInArrayAt(List.of(List.of("sheetNames"), List.of("sheetName")), "possibleValues"))
+ .isEqualTo(List.of(StringChoice.fromId("knime"), StringChoice.fromId("knime2")));
+ }
+
+ @Test
+ @SuppressWarnings("static-method")
+ void testSheetNamesAreLoadedFromPasswordProtectedExcelFile() throws IOException, URISyntaxException {
+ // given
+ var settings = new ExcelTableWriterNodeParameters();
+ var outputFile = new FSLocation(FSCategory.LOCAL, getTestFilePath(Fixtures.XLSX_ENC));
+ var outputFileSelection = new FileSelection(outputFile);
+ settings.m_outputFile = new LegacyFileWriterWithOverwritePolicyOptions(outputFileSelection, true,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.overwrite);
+ settings.m_encryption = new ExcelEncryptionSettings(ExcelEncryptionSettings.AuthType.PWD, null,
+ new Credentials("", Fixtures.TEST_PW));
+ var simulator = new DialogUpdateSimulator(settings, null);
+
+ // when
+ var result = simulator.simulateValueChange("outputFile", "file");
+
+ // then
+ assertThat(
+ result.getUiStateUpdateInArrayAt(List.of(List.of("sheetNames"), List.of("sheetName")), "possibleValues"))
+ .isEqualTo(List.of(StringChoice.fromId("knime"), StringChoice.fromId("knime2")));
+ }
+
+ private String getTestFilePath(final String path) throws IOException, URISyntaxException {
+ var url = FileLocator.toFileURL(getClass().getResource(path).toURI().toURL());
+ return FileUtil.getFileFromURL(url).toPath().toString();
+ }
+
+}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/ExcelEncryptionSettings.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/ExcelEncryptionSettings.java
new file mode 100644
index 00000000..8bb99e15
--- /dev/null
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/ExcelEncryptionSettings.java
@@ -0,0 +1,235 @@
+/*
+ * ------------------------------------------------------------------------
+ *
+ * Copyright by KNIME AG, Zurich, Switzerland
+ * Website: http://www.knime.com; Email: contact@knime.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, Version 3, as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs.
+ * Hence, KNIME and ECLIPSE are both independent programs and are not
+ * derived from each other. Should, however, the interpretation of the
+ * GNU GPL Version 3 ("License") under any applicable laws result in
+ * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants
+ * you the additional permission to use and propagate KNIME together with
+ * ECLIPSE with only the license terms in place for ECLIPSE applying to
+ * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the
+ * license terms of ECLIPSE themselves allow for the respective use and
+ * propagation of ECLIPSE together with KNIME.
+ *
+ * Additional permission relating to nodes for KNIME that extend the Node
+ * Extension (and in particular that are based on subclasses of NodeModel,
+ * NodeDialog, and NodeView) and that only interoperate with KNIME through
+ * standard APIs ("Nodes"):
+ * Nodes are deemed to be separate and independent programs and to not be
+ * covered works. Notwithstanding anything to the contrary in the
+ * License, the License does not apply to Nodes, you are not required to
+ * license Nodes under the License, and you are granted a license to
+ * prepare and propagate Nodes, in each case even if such Nodes are
+ * propagated with or for interoperation with KNIME. The owner of a Node
+ * may freely choose the license terms applicable to such Node, including
+ * when such Node is propagated with or for interoperation with KNIME.
+ * ---------------------------------------------------------------------
+ *
+ * History
+ * 11 Dec 2025 (Thomas Reifenberger): created
+ */
+package org.knime.ext.poi3.node.io.filehandling.excel;
+
+import java.util.List;
+
+import org.knime.core.node.InvalidSettingsException;
+import org.knime.core.node.NodeSettingsRO;
+import org.knime.core.node.NodeSettingsWO;
+import org.knime.core.node.defaultnodesettings.SettingsModelAuthentication;
+import org.knime.core.node.workflow.FlowVariable;
+import org.knime.core.node.workflow.VariableType;
+import org.knime.core.webui.node.dialog.defaultdialog.NodeParametersInputImpl;
+import org.knime.node.parameters.NodeParameters;
+import org.knime.node.parameters.NodeParametersInput;
+import org.knime.node.parameters.Widget;
+import org.knime.node.parameters.persistence.NodeParametersPersistor;
+import org.knime.node.parameters.persistence.Persist;
+import org.knime.node.parameters.persistence.Persistor;
+import org.knime.node.parameters.updates.Effect;
+import org.knime.node.parameters.updates.EffectPredicate;
+import org.knime.node.parameters.updates.EffectPredicateProvider;
+import org.knime.node.parameters.updates.ParameterReference;
+import org.knime.node.parameters.updates.ValueReference;
+import org.knime.node.parameters.widget.choices.ChoicesProvider;
+import org.knime.node.parameters.widget.choices.FlowVariableChoicesProvider;
+import org.knime.node.parameters.widget.choices.Label;
+import org.knime.node.parameters.widget.choices.ValueSwitchWidget;
+import org.knime.node.parameters.widget.credentials.Credentials;
+import org.knime.node.parameters.widget.credentials.PasswordWidget;
+
+/**
+ *
+ * Widget to manage Excel file encryption settings compatible with the legacy {@link SettingsModelAuthentication}, which
+ * is used in various Excel reader and writer models. Only the parts used in those Excel nodes are supported (password &
+ * a subset of the AuthTypes).
+ *
+ * @author Thomas Reifenberger, TNG Technology Consulting GmbH
+ */
+public class ExcelEncryptionSettings implements NodeParameters {
+
+ private static final String SETTINGS_MODEL_KEY_USERNAME = "username";
+
+ private static final String SETTINGS_MODEL_KEY_PASSWORD = "password";
+
+ private static final String SECRET_KEY = "c-rH4Tkyk";
+
+ /**
+ * Default constructor
+ */
+ public ExcelEncryptionSettings() {
+ }
+
+ /**
+ * All-args constructor
+ *
+ * @param type the authentication type
+ * @param credentials Name of the workflow credentials flow variable to use if type is CREDENTIALS
+ * @param password The password to use if type is PWD
+ */
+ public ExcelEncryptionSettings(final AuthType type, final String credentials, final Credentials password) {
+ m_type = type;
+ m_credentials = credentials;
+ m_password = password;
+ }
+
+ /**
+ * The authentication types supported for Excel file encryption.
+ */
+ public enum AuthType {
+ /**
+ * No password protection.
+ */
+ @Label(value = "None", description = "Only files without password protection can be updated.")
+ NONE, //
+ /**
+ * Password protection via workflow credentials.
+ */
+ @Label(value = "Credentials", description = "Use a password set via workflow credentials.")
+ CREDENTIALS, //
+ /**
+ * Password protection.
+ */
+ @Label(value = "Password", description = "Specify a password.")
+ PWD, //
+ ;
+ }
+
+ @Widget(title = "Password to protect files",
+ description = "Allows you to specify a password to protect the output file with. "
+ + "In case the \"append\" option is selected and the file already exists, the password must be valid "
+ + "for the existing file.")
+ @ValueSwitchWidget
+ @Persist(configKey = "selectedType")
+ @ValueReference(AuthTypeRef.class)
+ AuthType m_type = AuthType.NONE;
+
+ interface AuthTypeRef extends ParameterReference {
+ }
+
+ private static class IsCredentialsSelected implements EffectPredicateProvider {
+ @Override
+ public EffectPredicate init(final PredicateInitializer i) {
+ return i.getEnum(AuthTypeRef.class).isOneOf(AuthType.CREDENTIALS);
+ }
+ }
+
+ private static class IsPasswordSelected implements EffectPredicateProvider {
+ @Override
+ public EffectPredicate init(final PredicateInitializer i) {
+ return i.getEnum(AuthTypeRef.class).isOneOf(AuthType.PWD);
+ }
+ }
+
+ @Widget(title = "Workflow credentials", description = "Select the workflow credentials to use.")
+ @Effect(predicate = IsCredentialsSelected.class, type = Effect.EffectType.SHOW)
+ @Persist(configKey = "credentials")
+ @ChoicesProvider(CredentialFlowVariablesProvider.class)
+ String m_credentials = "";
+
+ @Widget(title = "Password", description = "Enter the password to protect the file.")
+ @Effect(predicate = IsPasswordSelected.class, type = Effect.EffectType.SHOW)
+ @Persistor(CredentialsPersistor.class)
+ @PasswordWidget
+ Credentials m_password = new Credentials();
+
+ private static final class CredentialFlowVariablesProvider implements FlowVariableChoicesProvider {
+
+ @Override
+ public List flowVariableChoices(final NodeParametersInput context) {
+ return context.getAvailableInputFlowVariables(VariableType.CredentialsType.INSTANCE).values().stream()
+ .toList();
+ }
+
+ }
+
+ private static final class CredentialsPersistor implements NodeParametersPersistor {
+
+ @Override
+ public Credentials load(final NodeSettingsRO settings) throws InvalidSettingsException {
+ final var password = settings.getPassword(SETTINGS_MODEL_KEY_PASSWORD, SECRET_KEY, null);
+ return new Credentials(null, password);
+ }
+
+ @Override
+ public void save(final Credentials param, final NodeSettingsWO settings) {
+ // username is not used, but needs to be present for compatibility with model
+ settings.addString(SETTINGS_MODEL_KEY_USERNAME, null);
+ settings.addPassword(SETTINGS_MODEL_KEY_PASSWORD, SECRET_KEY, param.getPassword());
+ }
+
+ @Override
+ public String[][] getConfigPaths() {
+ return new String[][]{{}}; // No flow variables for weakly encrypted password fields
+ }
+
+ }
+
+ /**
+ * Get the password according to the selected authentication type to be used to access referenced excel file to
+ * populate the UI (e.g. loading available sheet names for a input field)
+ *
+ * @param context the node parameters input context
+ * @return the password or null if no password is set
+ */
+ @SuppressWarnings("restriction")
+ public String getPassword(final NodeParametersInput context) {
+ if (m_type == ExcelEncryptionSettings.AuthType.NONE) {
+ return null;
+ } else if (m_type == ExcelEncryptionSettings.AuthType.PWD) {
+ return m_password.getPassword();
+ } else if (m_type == ExcelEncryptionSettings.AuthType.CREDENTIALS) {
+ var flowVariableName = m_credentials;
+ var credentialsProvider = ((NodeParametersInputImpl)context).getCredentialsProvider();
+
+ try {
+ if (credentialsProvider.isPresent()) {
+ return credentialsProvider.get().get(flowVariableName).getPassword();
+ }
+ return null;
+ } catch (IllegalArgumentException e) { // Flow variable not found
+ return null;
+ }
+ } else {
+ throw new IllegalStateException("Unknown authentication type: " + m_type);
+ }
+ }
+
+}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterConfig.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterConfig.java
index 00522a24..d02d981c 100644
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterConfig.java
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterConfig.java
@@ -86,37 +86,37 @@ final class ExcelTableWriterConfig implements ExcelTableConfig {
private static final SheetNameExistsHandling DEFAULT_SHEET_EXISTS_HANDLING = SheetNameExistsHandling.FAIL;
- private static final String CFG_EXCEL_FORMAT = "excel_format";
+ static final String CFG_EXCEL_FORMAT = "excel_format";
- private static final String CFG_FILE_CHOOSER = "file_selection";
+ static final String CFG_FILE_CHOOSER = "file_selection";
private static final String DEFAULT_SHEET_NAME_PREFIX = "default_";
- private static final String CFG_SHEET_NAMES = "sheet_names";
+ static final String CFG_SHEET_NAMES = "sheet_names";
- private static final String CFG_SHEET_EXISTS = "if_sheet_exists";
+ static final String CFG_SHEET_EXISTS = "if_sheet_exists";
- private static final String CFG_WRITE_ROW_KEY = "write_row_key";
+ static final String CFG_WRITE_ROW_KEY = "write_row_key";
- private static final String CFG_WRITE_COLUMN_HEADER = "write_column_header";
+ static final String CFG_WRITE_COLUMN_HEADER = "write_column_header";
- private static final String CFG_SKIP_COLUMN_HEADER_ON_APPEND = "skip_column_header_on_append";
+ static final String CFG_SKIP_COLUMN_HEADER_ON_APPEND = "skip_column_header_on_append";
- private static final String CFG_MISSING_VALUE_PATTERN = "missing_value_pattern";
+ static final String CFG_MISSING_VALUE_PATTERN = "missing_value_pattern";
- private static final String CFG_REPLACE_MISSINGS = "replace_missings";
+ static final String CFG_REPLACE_MISSINGS = "replace_missings";
- private static final String CFG_LANDSCAPE = "layout";
+ static final String CFG_LANDSCAPE = "layout";
- private static final String CFG_AUTOSIZE = "autosize_columns";
+ static final String CFG_AUTOSIZE = "autosize_columns";
- private static final String CFG_PAPER_SIZE = "paper_size";
+ static final String CFG_PAPER_SIZE = "paper_size";
- private static final String CFG_EVALUATE_FORMULAS = "evaluate_formulas";
+ static final String CFG_EVALUATE_FORMULAS = "evaluate_formulas";
- private static final String CFG_OPEN_FILE_AFTER_EXEC = "open_file_after_exec" + SettingsModel.CFGKEY_INTERNAL;
+ static final String CFG_OPEN_FILE_AFTER_EXEC = "open_file_after_exec" + SettingsModel.CFGKEY_INTERNAL;
- private static final String CFG_AUTHENTICATION_METHOD = "authentication_method";
+ static final String CFG_AUTHENTICATION_METHOD = "authentication_method";
private final SettingsModelString m_excelFormat;
@@ -226,7 +226,7 @@ SettingsModelAuthentication getAuthentication() {
return m_authenticationSettingsModel;
}
- private static String createDefaultSheetName(final int idx) {
+ static String createDefaultSheetName(final int idx) {
return DEFAULT_SHEET_NAME_PREFIX + (idx + 1);
}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeDialog.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeDialog.java
deleted file mode 100644
index 821aec0e..00000000
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeDialog.java
+++ /dev/null
@@ -1,656 +0,0 @@
-/*
- * ------------------------------------------------------------------------
- *
- * Copyright by KNIME AG, Zurich, Switzerland
- * Website: http://www.knime.com; Email: contact@knime.com
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License, Version 3, as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, see .
- *
- * Additional permission under GNU GPL version 3 section 7:
- *
- * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs.
- * Hence, KNIME and ECLIPSE are both independent programs and are not
- * derived from each other. Should, however, the interpretation of the
- * GNU GPL Version 3 ("License") under any applicable laws result in
- * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants
- * you the additional permission to use and propagate KNIME together with
- * ECLIPSE with only the license terms in place for ECLIPSE applying to
- * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the
- * license terms of ECLIPSE themselves allow for the respective use and
- * propagation of ECLIPSE together with KNIME.
- *
- * Additional permission relating to nodes for KNIME that extend the Node
- * Extension (and in particular that are based on subclasses of NodeModel,
- * NodeDialog, and NodeView) and that only interoperate with KNIME through
- * standard APIs ("Nodes"):
- * Nodes are deemed to be separate and independent programs and to not be
- * covered works. Notwithstanding anything to the contrary in the
- * License, the License does not apply to Nodes, you are not required to
- * license Nodes under the License, and you are granted a license to
- * prepare and propagate Nodes, in each case even if such Nodes are
- * propagated with or for interoperation with KNIME. The owner of a Node
- * may freely choose the license terms applicable to such Node, including
- * when such Node is propagated with or for interoperation with KNIME.
- * ---------------------------------------------------------------------
- *
- * History
- * Nov 6, 2020 (Mark Ortmann, KNIME GmbH, Berlin, Germany): created
- */
-package org.knime.ext.poi3.node.io.filehandling.excel.writer;
-
-import java.awt.Component;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.nio.file.Files;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-
-import javax.swing.BorderFactory;
-import javax.swing.Box;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.apache.poi.EncryptedDocumentException;
-import org.knime.core.node.InvalidSettingsException;
-import org.knime.core.node.NodeDialogPane;
-import org.knime.core.node.NodeLogger;
-import org.knime.core.node.NodeSettingsRO;
-import org.knime.core.node.NodeSettingsWO;
-import org.knime.core.node.NotConfigurableException;
-import org.knime.core.node.context.ports.PortsConfiguration;
-import org.knime.core.node.defaultnodesettings.DialogComponentAuthentication;
-import org.knime.core.node.defaultnodesettings.DialogComponentBoolean;
-import org.knime.core.node.defaultnodesettings.DialogComponentButtonGroup;
-import org.knime.core.node.defaultnodesettings.DialogComponentString;
-import org.knime.core.node.defaultnodesettings.DialogComponentStringSelection;
-import org.knime.core.node.defaultnodesettings.SettingsModelAuthentication;
-import org.knime.core.node.defaultnodesettings.SettingsModelAuthentication.AuthenticationType;
-import org.knime.core.node.defaultnodesettings.SettingsModelString;
-import org.knime.core.node.port.PortObjectSpec;
-import org.knime.core.node.util.SharedIcons;
-import org.knime.core.util.SwingWorkerWithContext;
-import org.knime.ext.poi3.node.io.filehandling.excel.CryptUtil;
-import org.knime.ext.poi3.node.io.filehandling.excel.DecryptionAwareWriterStatusMessageReporter;
-import org.knime.ext.poi3.node.io.filehandling.excel.DialogUtil;
-import org.knime.ext.poi3.node.io.filehandling.excel.StatusMessageReporterChain;
-import org.knime.ext.poi3.node.io.filehandling.excel.reader.read.ExcelUtils;
-import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.ExcelConstants;
-import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.ExcelFormat;
-import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.Orientation;
-import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.PaperSize;
-import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.SheetNameExistsHandling;
-import org.knime.filehandling.core.connections.FSFiles;
-import org.knime.filehandling.core.connections.FSLocation;
-import org.knime.filehandling.core.data.location.variable.FSLocationVariableType;
-import org.knime.filehandling.core.defaultnodesettings.filechooser.StatusMessageReporter;
-import org.knime.filehandling.core.defaultnodesettings.filechooser.writer.DefaultWriterStatusMessageReporter;
-import org.knime.filehandling.core.defaultnodesettings.filechooser.writer.DialogComponentWriterFileChooser;
-import org.knime.filehandling.core.defaultnodesettings.filechooser.writer.FileOverwritePolicy;
-import org.knime.filehandling.core.defaultnodesettings.filechooser.writer.SettingsModelWriterFileChooser;
-import org.knime.filehandling.core.defaultnodesettings.status.StatusMessage;
-import org.knime.filehandling.core.util.GBCBuilder;
-
-/**
- * The dialog of the 'Excel Table Writer' node.
- *
- * @author Mark Ortmann, KNIME GmbH, Berlin, Germany
- */
-final class ExcelTableWriterNodeDialog extends NodeDialogPane {
-
- private static final NodeLogger LOGGER = NodeLogger.getLogger(ExcelTableWriterNodeDialog.class);
-
- private SheetUpdater m_updateSheet;
-
- private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile(//
- Arrays.stream(ExcelFormat.values())//
- .map(ExcelFormat::getFileExtension)//
- .collect(Collectors.joining("|", "(", ")\\s*$")),
- Pattern.CASE_INSENSITIVE);
-
- private final ExcelTableWriterConfig m_cfg;
-
- private final DialogComponentStringSelection m_excelType;
-
- private final DialogComponentWriterFileChooser m_fileChooser;
-
- private final JComboBox[] m_sheetNames;
-
- private final JLabel m_sheetNamesUpdateErr;
-
- private final DialogComponentButtonGroup m_sheetNameCollisionHandling;
-
- private final DialogComponentBoolean m_writeRowKey;
-
- private final DialogComponentBoolean m_writeColHeader;
-
- private final DialogComponentBoolean m_skipColumnHeaderOnAppend;
-
- private final DialogComponentBoolean m_replaceMissings;
-
- private final DialogComponentString m_missingValPattern;
-
- private final DialogComponentBoolean m_evaluateFormulas;
-
- private final DialogComponentButtonGroup m_landscape;
-
- private final DialogComponentBoolean m_autoSize;
-
- private final JComboBox m_paperSize;
-
- private final DialogComponentBoolean m_openFileAfterExec;
-
- private final JLabel m_openFileAfterExecLbl;
-
- private final DialogComponentAuthentication m_passwordComponent;
-
- private final SettingsModelAuthentication m_authenticationSettingsModel;
-
- static final String CFG_PASSWORD = "password";
-
- /**
- * Constructor.
- *
- * @param portsConfig the ports configuration
- */
- @SuppressWarnings("unchecked")
- ExcelTableWriterNodeDialog(final PortsConfiguration portsConfig) {
- m_cfg = new ExcelTableWriterConfig(portsConfig);
-
- SettingsModelString excelFormatModel = m_cfg.getExcelFormatModel();
- m_excelType = new DialogComponentStringSelection(excelFormatModel, "Excel format ",
- Arrays.stream(ExcelFormat.values())//
- .map(ExcelFormat::name)//
- .toList());
-
- final SettingsModelWriterFileChooser writerModel = m_cfg.getFileChooserModel();
- final var writeFvm =
- createFlowVariableModel(writerModel.getKeysForFSLocation(), FSLocationVariableType.INSTANCE);
-
- m_authenticationSettingsModel = m_cfg.getAuthentication();
-
- final Supplier passwordProvider =
- () -> CryptUtil.getPassword(m_authenticationSettingsModel, getCredentialsProvider());
-
- final Function reporter =
- fc -> StatusMessageReporterChain
- . first(new DefaultWriterStatusMessageReporter(fc))
- // no need to check if the default reporter has a problem (e.g. file does not exist)
- .successAnd(
- // if we don't append, we do not need to check that the password can be used to decrypt
- // the existing file
- model -> FileOverwritePolicy.APPEND == model.getFileOverwritePolicy(),
- new DecryptionAwareWriterStatusMessageReporter(fc, passwordProvider, m_cfg::getExcelFormat,
- DialogUtil::decryptionErrorHandler))
- .build(fc);
-
- m_fileChooser = new DialogComponentWriterFileChooser(writerModel, "excel_reader_writer", writeFvm, reporter);
-
- m_sheetNames = Stream.generate(JComboBox::new)//
- .limit(portsConfig.getInputPortLocation().get(ExcelTableWriterNodeFactory.SHEET_GRP_ID).length)//
- .toArray(JComboBox[]::new);
- Arrays.stream(m_sheetNames).forEach(b -> b.setEditable(true));
-
- m_sheetNamesUpdateErr = new JLabel();
- m_sheetNamesUpdateErr.setVisible(false);
-
- m_sheetNameCollisionHandling = new DialogComponentButtonGroup(m_cfg.getSheetExistsHandlingModel(), null, false,
- SheetNameExistsHandling.values());
-
- m_writeColHeader = new DialogComponentBoolean(m_cfg.getWriteColHeaderModel(), "Write column headers");
- m_skipColumnHeaderOnAppend = new DialogComponentBoolean(m_cfg.getSkipColumnHeaderOnAppendModel(),
- "Don't write column headers if sheet exists");
- m_cfg.getWriteColHeaderModel().addChangeListener(
- e -> m_cfg.getSkipColumnHeaderOnAppendModel().setEnabled(m_cfg.getWriteColHeaderModel().getBooleanValue()));
-
- m_writeRowKey = new DialogComponentBoolean(m_cfg.getWriteRowKeyModel(), "Write row key");
- m_replaceMissings = new DialogComponentBoolean(m_cfg.getReplaceMissingsModel(), "Replace missing values by");
- m_missingValPattern = new DialogComponentString(m_cfg.getMissingValPatternModel(), null);
-
- m_evaluateFormulas = new DialogComponentBoolean(m_cfg.getEvaluateFormulasModel(),
- "Evaluate formulas (leave unchecked if uncertain; see node description for details)");
-
- m_autoSize = new DialogComponentBoolean(m_cfg.getAutoSizeModel(), "Autosize columns");
- m_landscape = new DialogComponentButtonGroup(m_cfg.getLandscapeModel(), null, false, Orientation.values());
- m_paperSize = new JComboBox<>(PaperSize.values());
- m_openFileAfterExec =
- new DialogComponentBoolean(m_cfg.getOpenFileAfterExecModel(), "Open file after execution");
- m_openFileAfterExecLbl = new JLabel("");
-
- writerModel.addChangeListener(l -> toggleOptions());
- excelFormatModel.addChangeListener(l -> updateLocation());
-
- addTab("Settings", createSettings());
-
- m_passwordComponent = new DialogComponentAuthentication(m_authenticationSettingsModel, null,
- AuthenticationType.PWD, AuthenticationType.CREDENTIALS, AuthenticationType.NONE);
- m_passwordComponent.getModel().addChangeListener(e -> m_fileChooser.updateComponent());
- addTab("Encryption", createEncryptionSettingsTab());
- }
-
- private void toggleOptions() {
- toggleAppendRelatedOptions();
- toggleOpenFileAfterExecOption();
- updateSheetListAndSelect();
- }
-
- private void toggleAppendRelatedOptions() {
- final boolean enabled = m_fileChooser.getSettingsModel().getFileOverwritePolicy() == FileOverwritePolicy.APPEND;
- m_evaluateFormulas.getModel().setEnabled(enabled);
- m_sheetNameCollisionHandling.getModel().setEnabled(enabled);
- // Show decryption errors only if the file would be read (no reading on create/overwrite)
- m_sheetNamesUpdateErr.setVisible(enabled);
- }
-
- private void toggleOpenFileAfterExecOption() {
- // cannot be headless as we'd not have a dialog in this case
- final boolean isRemote = ExcelTableWriterNodeModel.isHeadlessOrRemote();
- final boolean categorySupported = ExcelTableWriterNodeModel
- .categoryIsSupported(m_fileChooser.getSettingsModel().getLocation().getFSCategory());
- m_openFileAfterExec.getModel().setEnabled(!isRemote && categorySupported);
- if (isRemote) {
- m_openFileAfterExecLbl.setIcon(SharedIcons.INFO_BALLOON.get());
- m_openFileAfterExecLbl.setText("Not support in remote job view");
- } else if (!categorySupported) {
- m_openFileAfterExecLbl.setIcon(SharedIcons.INFO_BALLOON.get());
- m_openFileAfterExecLbl.setText("Not support by the selected file system");
- } else {
- m_openFileAfterExecLbl.setIcon(null);
- m_openFileAfterExecLbl.setText("");
- }
- }
-
- private void updateLocation() {
- final var format = ExcelFormat.valueOf(m_cfg.getExcelFormatModel().getStringValue());
- SettingsModelWriterFileChooser writerModel = m_fileChooser.getSettingsModel();
- if (!writerModel.isOverwrittenByFlowVariable()) {
- FSLocation location = writerModel.getLocation();
- final String locPath = location.getPath();
- final String newPath = FILE_EXTENSION_PATTERN.matcher(locPath).replaceAll(format.getFileExtension());
- if (!newPath.equals(locPath)) {
- writerModel.setLocation(
- new FSLocation(location.getFSCategory(), location.getFileSystemSpecifier().orElse(null), newPath));
- }
- }
- writerModel.setFileExtensions(format.getFileExtension());
- }
-
- private Component createSettings() {
- final var p = new JPanel(new GridBagLayout());
- final var gbc = new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(1).fillHorizontal().setWidth(2);
- p.add(createFileChooserPanel(), gbc.build());
-
- gbc.incY();
- p.add(createSheetNamesPanel(), gbc.build());
-
- gbc.incY();
- p.add(createNameIdPanel(), gbc.build());
-
- gbc.incY();
- p.add(createMissingsPanel(), gbc.build());
-
- gbc.incY();
- p.add(createFormulasPanel(), gbc.build());
-
- gbc.incY();
- p.add(createSizePanel(), gbc.build());
-
- gbc.incY().setWeightX(0).fillNone().setWidth(1);
- p.add(m_openFileAfterExec.getComponentPanel(), gbc.build());
-
- gbc.incX();
- p.add(m_openFileAfterExecLbl, gbc.build());
-
- gbc.resetX().incY().setWidth(2).weight(1, 1).fillBoth();
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- private Component createFileChooserPanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(
- BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "File format & output location"));
- final var gbc = new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone();
-
- p.add(m_excelType.getComponentPanel(), gbc.build());
-
- gbc.incY().setWeightX(1).fillHorizontal().insetLeft(4);
- p.add(m_fileChooser.getComponentPanel(), gbc.build());
-
- return p;
- }
-
- private Component createSheetNamesPanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Sheets"));
- final var gbc = new GBCBuilder().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone();
-
- for (var i = 0; i < m_sheetNames.length; i++) {
- gbc.resetX().incY().insetLeft(4);
- p.add(new JLabel((i + 1) + ". sheet name"), gbc.build());
-
- gbc.incX().insetLeft(15);
- p.add(m_sheetNames[i], gbc.build());
- gbc.insetTop(3);
-
- }
-
- gbc.incY().resetX().insetLeft(4);
- p.add(new JLabel("If sheet exists"), gbc.build());
-
- gbc.incX().insetLeft(5);
- p.add(m_sheetNameCollisionHandling.getComponentPanel(), gbc.build());
-
- gbc.incY().fillHorizontal();
- p.add(m_sheetNamesUpdateErr, gbc.build());
- m_fileChooser.getSettingsModel().addChangeListener(e ->
- // if we never read the file (for append), the current password does not matter
- m_sheetNamesUpdateErr
- .setVisible(m_fileChooser.getSettingsModel().getFileOverwritePolicy() == FileOverwritePolicy.APPEND));
-
- gbc.resetX().setWeightX(1).setWidth(2).incY().insetLeft(0).insetTop(0).fillHorizontal();
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- private Component createNameIdPanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Names and IDs"));
- final var gbc =
- new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone().insetLeft(-3);
- p.add(m_writeRowKey.getComponentPanel(), gbc.build());
-
- gbc.incY().insetTop(-5);
- p.add(m_writeColHeader.getComponentPanel(), gbc.build());
- gbc.incY();
- p.add(m_skipColumnHeaderOnAppend.getComponentPanel(), gbc.build());
-
- gbc.incY().setWeightX(1).fillHorizontal();
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- private Component createMissingsPanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Missing value handling"));
-
- final var gbc =
- new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone().insetLeft(-3);
- p.add(m_replaceMissings.getComponentPanel(), gbc.build());
-
- gbc.incX().insetLeft(-10);
- p.add(m_missingValPattern.getComponentPanel(), gbc.build());
-
- gbc.incY().setWidth(2).setWeightX(1).fillHorizontal().insetTop(-10);
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- private Component createFormulasPanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Formulas"));
-
- final var gbc =
- new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone().insetLeft(-3);
- p.add(m_evaluateFormulas.getComponentPanel(), gbc.build());
-
- gbc.incY().setWeightX(1).fillHorizontal().insetTop(-10);
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- private Component createSizePanel() {
- final var p = new JPanel(new GridBagLayout());
- p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Layout"));
-
- final var gbc = new GBCBuilder().resetX().resetY().anchorLineStart().setWeightX(0).setWeightY(0).fillNone()
- .setWidth(2).insetLeft(-3);
- p.add(m_autoSize.getComponentPanel(), gbc.build());
-
- gbc.incY().insetTop(-5).setWidth(1);
- p.add(m_landscape.getComponentPanel(), gbc.build());
-
- gbc.incX().insetLeft(12);
- p.add(m_paperSize, gbc.build());
-
- gbc.incY().setWidth(2).setWeightX(1).fillHorizontal();
- p.add(new JPanel(), gbc.build());
-
- return p;
- }
-
- // Encryption Tab
- private JPanel createEncryptionSettingsTab() {
- final var panel = new JPanel(new GridBagLayout());
- final var gbcBuilder =
- new GBCBuilder().resetPos().anchorFirstLineStart().setWeightX(1).setWeightY(1).fillHorizontal();
- panel.add(createEncryptionPanel(), gbcBuilder.build());
- return panel;
- }
-
- private JPanel createEncryptionPanel() {
- final var panel = new JPanel(new GridBagLayout());
- panel.setBorder(
- BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Password to protect files"));
- final var gbcBuilder = new GBCBuilder(new Insets(0, 5, 0, 5)).resetPos().anchorFirstLineStart().fillBoth();
- panel.add(m_passwordComponent.getComponentPanel(), gbcBuilder.build());
- m_passwordComponent.getModel().addChangeListener(e -> {
- m_sheetNamesUpdateErr.setVisible(false);
- updateSheetListAndSelect();
- });
- panel.add(Box.createHorizontalBox(), gbcBuilder.incX().setWeightX(1).build());
- return panel;
- }
-
- @Override
- protected void saveSettingsTo(final NodeSettingsWO settings) throws InvalidSettingsException {
- m_excelType.saveSettingsTo(settings);
- m_fileChooser.saveSettingsTo(settings);
- synchronized (m_sheetNames) {
- ExcelTableWriterConfig.saveSheetNames(settings, Arrays.stream(m_sheetNames)//
- .map(JComboBox::getSelectedItem)//
- .map(String.class::cast)//
- .map(String::trim)//
- .toArray(String[]::new));
- }
- m_sheetNameCollisionHandling.saveSettingsTo(settings);
- m_writeRowKey.saveSettingsTo(settings);
- m_writeColHeader.saveSettingsTo(settings);
- m_skipColumnHeaderOnAppend.saveSettingsTo(settings);
- m_replaceMissings.saveSettingsTo(settings);
- m_missingValPattern.saveSettingsTo(settings);
- m_evaluateFormulas.saveSettingsTo(settings);
- m_autoSize.saveSettingsTo(settings);
- m_landscape.saveSettingsTo(settings);
- final SettingsModelString paperSizeModel = m_cfg.getPaperSizeModel();
- paperSizeModel.setStringValue(((PaperSize)m_paperSize.getSelectedItem()).name());
- paperSizeModel.saveSettingsTo(settings);
- m_openFileAfterExec.saveSettingsTo(settings);
- m_authenticationSettingsModel.saveSettingsTo(settings);
- m_passwordComponent.saveSettingsTo(settings);
- }
-
- @Override
- protected void loadSettingsFrom(final NodeSettingsRO settings, final PortObjectSpec[] specs)
- throws NotConfigurableException {
- m_excelType.loadSettingsFrom(settings, specs);
- m_fileChooser.loadSettingsFrom(settings, specs);
- m_cfg.loadSheetsInDialog(settings);
- synchronized (m_sheetNames) {
- final var sheetNames = m_cfg.getSheetNames();
- IntStream.range(0, m_sheetNames.length)//
- .forEach(i -> {
- final var name = sheetNames[i];
- m_sheetNames[i].setSelectedItem(name);
- });
- }
- m_sheetNameCollisionHandling.loadSettingsFrom(settings, specs);
- m_writeRowKey.loadSettingsFrom(settings, specs);
- m_writeColHeader.loadSettingsFrom(settings, specs);
- try {
- m_cfg.getSkipColumnHeaderOnAppendModel().loadSettingsFrom(settings);
- m_skipColumnHeaderOnAppend.loadSettingsFrom(settings, specs);
- } catch (InvalidSettingsException e) { // NOSONAR we want to load the default here
- m_cfg.getSkipColumnHeaderOnAppendModel().setBooleanValue(true);
- }
- m_cfg.getSkipColumnHeaderOnAppendModel().setEnabled(m_cfg.getWriteColHeaderModel().getBooleanValue());
- m_replaceMissings.loadSettingsFrom(settings, specs);
- m_missingValPattern.loadSettingsFrom(settings, specs);
- m_evaluateFormulas.loadSettingsFrom(settings, specs);
- m_autoSize.loadSettingsFrom(settings, specs);
- m_landscape.loadSettingsFrom(settings, specs);
- final SettingsModelString paperSizeModel = m_cfg.getPaperSizeModel();
- try {
- paperSizeModel.loadSettingsFrom(settings);
- } catch (InvalidSettingsException e) { // NOSONAR we want to load the default here
- paperSizeModel.setStringValue(ExcelConstants.DEFAULT_PAPER_SIZE.name());
- }
- m_paperSize.setSelectedItem(PaperSize.valueOf(paperSizeModel.getStringValue()));
- m_openFileAfterExec.loadSettingsFrom(settings, specs);
-
- try {
- m_authenticationSettingsModel.loadSettingsFrom(settings);
- } catch (InvalidSettingsException e) { // NOSONAR we want to load the default here
- m_authenticationSettingsModel.setValues(AuthenticationType.NONE, null, null, null);
- }
- // Needs only be loaded
- m_passwordComponent.loadSettingsFrom(settings, specs, getCredentialsProvider());
-
- toggleOptions();
- updateLocation();
- }
-
- /**
- * Reads from the currently selected file the list of worksheets (in a background thread) and selects the provided
- * sheet (if not null - otherwise selects the first name).
- */
- private void updateSheetListAndSelect() {
- cancelSheetUpdaterIfRunning();
- m_updateSheet = new SheetUpdater();
- m_updateSheet.execute();
- }
-
- private class SheetUpdater extends SwingWorkerWithContext, Void> {
- @Override
- protected List doInBackgroundWithContext() throws Exception {
- LOGGER.debug("Refreshing sheet names...");
- setSheetUpdateScanning();
- try (final var accessor = m_fileChooser.getSettingsModel().createWritePathAccessor()) {
- final var path = accessor.getOutputPath(this::logStatusMessage);
- if (!Files.exists(path)) {
- return Collections.emptyList();
- }
- final var pw = CryptUtil.getPassword(m_authenticationSettingsModel, getCredentialsProvider());
- try (final var in = FSFiles.newInputStream(path)) {
- return ExcelUtils.readSheetNames(in, pw);
- } catch (final EncryptedDocumentException e) {
- // provide an error message in the same style as the Excel Reader node
- throw new EncryptedDocumentException(
- "\"%s\" is password protected. Supply a valid password via the \"Encryption\" settings."
- .formatted(path),
- e);
- }
- }
- }
-
- @Override
- protected void doneWithContext() {
- if (isCancelled()) {
- return;
- }
-
- synchronized (m_sheetNames) {
- try {
- final var sheetNames = get();
- Arrays.stream(m_sheetNames).forEach(b -> {
- final var selected = b.getSelectedItem();
- b.removeAllItems();
- sheetNames.forEach(b::addItem);
- b.setSelectedItem(selected);
- });
- clearSheetUpdateError();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- clearSheetUpdateError();
- } catch (ExecutionException e) {
- final var showDecryptionError =
- m_fileChooser.getSettingsModel().getFileOverwritePolicy() == FileOverwritePolicy.APPEND;
- setSheetUpdateError(showDecryptionError, e);
- } catch (CancellationException e) {
- LOGGER.debug("Sheet update was cancelled", e);
- clearSheetUpdateError();
- }
- }
- }
-
- private void setSheetUpdateError(final boolean showDecryptionError, final Exception e) {
- final var cause = e.getCause();
- String statusMsg = null;
- if (cause != null) {
- statusMsg = cause.getMessage();
- }
- final var prefix = "Unable to read sheet names: ";
- m_sheetNamesUpdateErr.setIcon(SharedIcons.ERROR.get());
- m_sheetNamesUpdateErr.setText(prefix + (statusMsg == null ? "Reason unknown." : statusMsg));
- m_sheetNamesUpdateErr.setVisible(showDecryptionError);
- }
-
- private void setSheetUpdateScanning() {
- m_sheetNamesUpdateErr.setVisible(true);
- m_sheetNamesUpdateErr.setIcon(SharedIcons.INFO_BALLOON.get());
- m_sheetNamesUpdateErr.setText("Scanning...");
- }
-
- private void clearSheetUpdateError() {
- m_sheetNamesUpdateErr.setIcon(null);
- m_sheetNamesUpdateErr.setText(null);
- m_sheetNamesUpdateErr.setVisible(false);
- }
-
- private void logStatusMessage(final StatusMessage msg) {
- final var m = msg.getMessage();
- switch (msg.getType()) {
- case ERROR -> LOGGER.error(m);
- case INFO -> LOGGER.info(m);
- case WARNING -> LOGGER.warn(m);
- }
- }
- }
-
- @Override
- public void onClose() {
- m_fileChooser.onClose();
- cancelSheetUpdaterIfRunning();
- }
-
- private void cancelSheetUpdaterIfRunning() {
- if (m_updateSheet != null && !m_updateSheet.isDone()) {
- m_updateSheet.cancel(true);
- m_updateSheet = null;
- }
- }
-}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.java
index 856085dd..a647c623 100644
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.java
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.java
@@ -48,23 +48,43 @@
*/
package org.knime.ext.poi3.node.io.filehandling.excel.writer;
+import static org.knime.node.impl.description.PortDescription.dynamicPort;
+import static org.knime.node.impl.description.PortDescription.fixedPort;
+
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import org.knime.core.node.BufferedDataTable;
import org.knime.core.node.ConfigurableNodeFactory;
+import org.knime.core.node.NodeDescription;
import org.knime.core.node.NodeDialogPane;
import org.knime.core.node.NodeFactory;
import org.knime.core.node.NodeView;
import org.knime.core.node.context.NodeCreationConfiguration;
import org.knime.core.node.port.PortType;
+import org.knime.core.webui.node.dialog.NodeDialog;
+import org.knime.core.webui.node.dialog.NodeDialogFactory;
+import org.knime.core.webui.node.dialog.NodeDialogManager;
+import org.knime.core.webui.node.dialog.SettingsType;
+import org.knime.core.webui.node.dialog.defaultdialog.DefaultKaiNodeInterface;
+import org.knime.core.webui.node.dialog.defaultdialog.DefaultNodeDialog;
+import org.knime.core.webui.node.dialog.kai.KaiNodeInterface;
+import org.knime.core.webui.node.dialog.kai.KaiNodeInterfaceFactory;
import org.knime.filehandling.core.port.FileSystemPortObject;
+import org.knime.node.impl.description.DefaultNodeDescriptionUtil;
+import org.knime.node.impl.description.PortDescription;
/**
* {@link NodeFactory} creating the 'Excel Table Writer' node.
*
* @author Mark Ortmann, KNIME GmbH, Berlin, Germany
+ * @author Thomas Reifenberger, TNG Technology Consulting GmbH
+ * @author AI Migration Pipeline v1.2
*/
-public final class ExcelTableWriterNodeFactory extends ConfigurableNodeFactory {
+@SuppressWarnings("restriction")
+public final class ExcelTableWriterNodeFactory extends ConfigurableNodeFactory
+ implements NodeDialogFactory, KaiNodeInterfaceFactory {
/** The file system ports group id. */
static final String FS_CONNECT_GRP_ID = "File System Connection";
@@ -85,25 +105,99 @@ protected ExcelTableWriterNodeModel createNodeModel(final NodeCreationConfigurat
return new ExcelTableWriterNodeModel(creationConfig.getPortConfig().orElseThrow(IllegalStateException::new));
}
- @Override
- protected NodeDialogPane createNodeDialogPane(final NodeCreationConfiguration creationConfig) {
- return new ExcelTableWriterNodeDialog(creationConfig.getPortConfig().orElseThrow(IllegalStateException::new));
- }
-
@Override
protected int getNrNodeViews() {
return 0;
}
@Override
+ @SuppressWarnings("removal")
public NodeView createNodeView(final int viewIndex,
final ExcelTableWriterNodeModel nodeModel) {
return null;
}
@Override
+ @SuppressWarnings("removal")
protected boolean hasDialog() {
return true;
}
+ private static final String NODE_NAME = "Excel Writer";
+
+ private static final String NODE_ICON = "./excel_writer.png";
+
+ private static final String SHORT_DESCRIPTION = """
+ Writes data tables to an Excel file.
+ """;
+
+ @SuppressWarnings("java:S103")
+ private static final String FULL_DESCRIPTION =
+ """
+
This node writes the input data table into a spreadsheet of an Excel file, which can then be read
+ with other applications such as Microsoft Excel. The node can create completely new files or append data
+ to an existing Excel file. When appending, the input data can be appended as a new spreadsheet or after
+ the last row of an existing spreadsheet. By adding multiple data table input ports, the data can be
+ written/appended to multiple spreadsheets within the same file.
The node supports two formats
+ chosen by file extension:
.xls format: This is the file format which was used by
+ default up until Excel 2003. The maximum number of columns and rows held by a spreadsheet of this format
+ is 256 and 65536 respectively.
.xlsx format: The Office Open XML format is the file
+ format used by default from Excel 2007 onwards. The maximum number of columns and rows held by a
+ spreadsheet of this format is 16384 and 1048576 respectively.
If the data does not
+ fit into a single sheet, it will be split into multiple chunks that are written to newly chosen sheet
+ names sequentially. The new sheet names are derived from the originally selected sheet name by appending
+ " (i)" to it, where i=1,...,n. When appending to a file, a sheet may already exist. In this case
+ the node will (according to its settings) either replace the sheet, fail or append rows after the last
+ row in that sheet. This can be used to append data to an Excel file without having to create a new sheet
+ name once the original sheet is full by just selecting the original sheet name. The data will be
+ appendend to the last sheet in the name sequence. The original sheet name does not have to be changed.
+
This node does not support writing files in the '.xlsm' format, yet appending is supported.
+
This node can access a variety of differentfile
+ systems.More information about file handling in KNIME can be found in the officialFile Handling
+ Guide.
+ """;
+
+ private static final List INPUT_PORTS =
+ List.of(dynamicPort(FS_CONNECT_GRP_ID, "File system connection", """
+ The file system connection.
+ """), fixedPort("Input table", """
+ The data table to write.
+ """), dynamicPort(SHEET_GRP_ID, "Additional input tables", """
+ Additional data table to write.
+ """));
+
+ private static final List OUTPUT_PORTS = List.of();
+
+ private static final List KEYWORDS = List.of( //
+ "Spreadsheet", //
+ "XLS Writer", //
+ "Microsoft", //
+ "write excel file" //
+ );
+
+ @Override
+ @SuppressWarnings("removal")
+ public NodeDialogPane createNodeDialogPane(final NodeCreationConfiguration creationConfig) {
+ return NodeDialogManager.createLegacyFlowVariableNodeDialog(createNodeDialog());
+ }
+
+ @Override
+ public NodeDialog createNodeDialog() {
+ return new DefaultNodeDialog(SettingsType.MODEL, ExcelTableWriterNodeParameters.class);
+ }
+
+ @Override
+ public NodeDescription createNodeDescription() {
+ return DefaultNodeDescriptionUtil.createNodeDescription(NODE_NAME, NODE_ICON, INPUT_PORTS, OUTPUT_PORTS,
+ SHORT_DESCRIPTION, FULL_DESCRIPTION, List.of(), ExcelTableWriterNodeParameters.class, null, NodeType.Sink,
+ KEYWORDS, null);
+ }
+
+ @Override
+ public KaiNodeInterface createKaiNodeInterface() {
+ return new DefaultKaiNodeInterface(Map.of(SettingsType.MODEL, ExcelTableWriterNodeParameters.class));
+ }
+
}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.xml b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.xml
deleted file mode 100644
index ebd09456..00000000
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeFactory.xml
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
- Excel Writer
- Writes data tables to an Excel file.
-
-
-
-
-
This node writes the input data table into a spreadsheet of an Excel file, which can then
- be read with other applications such as Microsoft Excel.
- The node can create completely new files or append data to an existing Excel file.
- When appending, the input data can be appended as a new spreadsheet or after the last row of an existing spreadsheet.
- By adding multiple data table input ports, the data can be written/appended to multiple spreadsheets within the same file.
-
-
The node supports two formats chosen by file extension:
-
-
- .xls format: This is the file format which was used by default up until Excel 2003. The maximum
- number of columns and rows held by a spreadsheet of this format is
- 256 and 65536 respectively.
-
-
- .xlsx format: The Office Open XML format is the file format used by default from Excel 2007 onwards.
- The maximum number of columns and rows held by a spreadsheet of this format is 16384 and 1048576 respectively.
-
-
-
-
- If the data does not fit into a single sheet, it will be split into multiple chunks that are written
- to newly chosen sheet names sequentially. The new sheet names are derived from the originally
- selected sheet name by appending " (i)" to it, where i=1,...,n.
-
- When appending to a file, a sheet may already exist. In this case the node will
- (according to its settings) either replace the sheet, fail or append rows after the last row in that
- sheet. This can be used to append data to an Excel file without having to create a new sheet
- name once the original sheet is full by just selecting the original sheet name. The data will
- be appendend to the last sheet in the name sequence. The original sheet name does not have to be changed.
-
-
-
This node does not support writing files in the '.xlsm' format, yet appending is supported.
-
-
- This node can access a variety of different
- file systems.
- More information about file handling in KNIME can be found in the official
- File Handling Guide.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The data table to write.
-
- The file system connection.
-
-
- Additional data table to write.
-
-
-
- Spreadsheet
- XLS Writer
- Microsoft
- write excel file
-
-
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParameters.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParameters.java
new file mode 100644
index 00000000..2b570631
--- /dev/null
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/ExcelTableWriterNodeParameters.java
@@ -0,0 +1,721 @@
+/*
+ * ------------------------------------------------------------------------
+ *
+ * Copyright by KNIME AG, Zurich, Switzerland
+ * Website: http://www.knime.com; Email: contact@knime.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, Version 3, as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs.
+ * Hence, KNIME and ECLIPSE are both independent programs and are not
+ * derived from each other. Should, however, the interpretation of the
+ * GNU GPL Version 3 ("License") under any applicable laws result in
+ * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants
+ * you the additional permission to use and propagate KNIME together with
+ * ECLIPSE with only the license terms in place for ECLIPSE applying to
+ * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the
+ * license terms of ECLIPSE themselves allow for the respective use and
+ * propagation of ECLIPSE together with KNIME.
+ *
+ * Additional permission relating to nodes for KNIME that extend the Node
+ * Extension (and in particular that are based on subclasses of NodeModel,
+ * NodeDialog, and NodeView) and that only interoperate with KNIME through
+ * standard APIs ("Nodes"):
+ * Nodes are deemed to be separate and independent programs and to not be
+ * covered works. Notwithstanding anything to the contrary in the
+ * License, the License does not apply to Nodes, you are not required to
+ * license Nodes under the License, and you are granted a license to
+ * prepare and propagate Nodes, in each case even if such Nodes are
+ * propagated with or for interoperation with KNIME. The owner of a Node
+ * may freely choose the license terms applicable to such Node, including
+ * when such Node is propagated with or for interoperation with KNIME.
+ * ------------------------------------------------------------------------
+ */
+
+package org.knime.ext.poi3.node.io.filehandling.excel.writer;
+
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.knime.base.node.io.filehandling.webui.FileChooserPathAccessor;
+import org.knime.base.node.io.filehandling.webui.FileSystemPortConnectionUtil;
+import org.knime.core.node.InvalidSettingsException;
+import org.knime.core.node.NodeSettingsRO;
+import org.knime.core.node.NodeSettingsWO;
+import org.knime.core.webui.node.dialog.defaultdialog.internal.file.FileSelection;
+import org.knime.core.webui.node.dialog.defaultdialog.internal.file.FileWriterWidget;
+import org.knime.core.webui.node.dialog.defaultdialog.util.updates.StateComputationFailureException;
+import org.knime.core.webui.node.dialog.defaultdialog.widget.Modification;
+import org.knime.ext.poi3.node.io.filehandling.excel.CryptUtil;
+import org.knime.ext.poi3.node.io.filehandling.excel.ExcelEncryptionSettings;
+import org.knime.ext.poi3.node.io.filehandling.excel.reader.read.ExcelUtils;
+import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.Orientation;
+import org.knime.ext.poi3.node.io.filehandling.excel.writer.util.PaperSize;
+import org.knime.filehandling.core.connections.FSFiles;
+import org.knime.filehandling.core.connections.FSLocation;
+import org.knime.node.parameters.Advanced;
+import org.knime.node.parameters.NodeParameters;
+import org.knime.node.parameters.NodeParametersInput;
+import org.knime.node.parameters.Widget;
+import org.knime.node.parameters.array.ArrayWidget;
+import org.knime.node.parameters.array.ArrayWidget.ElementLayout;
+import org.knime.node.parameters.array.PerPortValueProvider;
+import org.knime.node.parameters.layout.After;
+import org.knime.node.parameters.layout.Layout;
+import org.knime.node.parameters.layout.Section;
+import org.knime.node.parameters.migration.LoadDefaultsForAbsentFields;
+import org.knime.node.parameters.migration.Migrate;
+import org.knime.node.parameters.persistence.NodeParametersPersistor;
+import org.knime.node.parameters.persistence.Persist;
+import org.knime.node.parameters.persistence.Persistor;
+import org.knime.node.parameters.persistence.legacy.LegacyFileWriterWithOverwritePolicyOptions;
+import org.knime.node.parameters.updates.Effect;
+import org.knime.node.parameters.updates.EffectPredicate;
+import org.knime.node.parameters.updates.EffectPredicateProvider;
+import org.knime.node.parameters.updates.ParameterReference;
+import org.knime.node.parameters.updates.StateProvider;
+import org.knime.node.parameters.updates.ValueProvider;
+import org.knime.node.parameters.updates.ValueReference;
+import org.knime.node.parameters.updates.legacy.LegacyPredicateInitializer;
+import org.knime.node.parameters.widget.choices.Label;
+import org.knime.node.parameters.widget.choices.StringChoicesProvider;
+import org.knime.node.parameters.widget.choices.SuggestionsProvider;
+import org.knime.node.parameters.widget.choices.ValueSwitchWidget;
+import org.knime.node.parameters.widget.message.TextMessage;
+import org.knime.node.parameters.widget.message.TextMessage.MessageType;
+import org.knime.node.parameters.widget.message.TextMessage.SimpleTextMessageProvider;
+import org.knime.node.parameters.widget.text.TextInputWidget;
+
+/**
+ * Node parameters for Excel Writer.
+ *
+ * @author Thomas Reifenberger, TNG Technology Consulting GmbH
+ * @author AI Migration Pipeline v1.2
+ */
+@SuppressWarnings("restriction")
+@LoadDefaultsForAbsentFields
+class ExcelTableWriterNodeParameters implements NodeParameters {
+
+ @Section(title = "Output")
+ interface OutputSection {
+ }
+
+ @Section(title = "Sheets")
+ @After(OutputSection.class)
+ interface SheetsSection {
+ }
+
+ @Section(title = "Headers / Keys")
+ @Advanced
+ @After(SheetsSection.class)
+ interface NamesAndIdsSection {
+ }
+
+ @Section(title = "Values")
+ @Advanced
+ @After(NamesAndIdsSection.class)
+ interface ValuesSection {
+ }
+
+ @Section(title = "Layout")
+ @Advanced
+ @After(ValuesSection.class)
+ interface LayoutSection {
+ }
+
+ @Section(title = "Interaction")
+ @Advanced
+ @After(LayoutSection.class)
+ interface InteractionSection {
+ }
+
+ @Widget(title = "Excel format", description = "Select the Excel file format to write.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_EXCEL_FORMAT)
+ @Layout(OutputSection.class)
+ @ValueSwitchWidget
+ @ValueReference(ExcelFormatRef.class)
+ ExcelFormat m_excelFormat = ExcelFormat.XLSX;
+
+ @Modification(OutputFileModification.class)
+ @Layout(OutputSection.class)
+ @Persist(configKey = ExcelTableWriterConfig.CFG_FILE_CHOOSER)
+ @ValueReference(OutputFileRef.class)
+ LegacyFileWriterWithOverwritePolicyOptions m_outputFile = new LegacyFileWriterWithOverwritePolicyOptions();
+
+ @Widget(title = "Password to protect files",
+ description = "If enabled, the output file will be password-protected. "
+ + "This supports XLS files (using weak encryption) and XLSX files (using AES encryption).")
+ @Layout(OutputSection.class)
+ @Persist(configKey = ExcelTableWriterConfig.CFG_AUTHENTICATION_METHOD)
+ @ValueReference(EncryptionRef.class)
+ ExcelEncryptionSettings m_encryption = new ExcelEncryptionSettings();
+
+ @TextMessage(EncryptionValidationMessageProvider.class)
+ @Layout(OutputSection.class)
+ @Effect(predicate = OutputFileOverwritePolicyIsAppend.class, type = Effect.EffectType.SHOW)
+ Void m_encryptionValidationMessage;
+
+ @Widget(title = "Sheets", description = "")
+ @ArrayWidget(hasFixedSize = true, elementDefaultValueProvider = SheetNamesDefaultProvider.class,
+ elementLayout = ElementLayout.HORIZONTAL_SINGLE_LINE)
+ @ValueProvider(SheetNamesValueProvider.class)
+ @Persistor(SheetNamesPersistor.class)
+ @Migrate
+ @Layout(SheetsSection.class)
+ @ValueReference(SheetNamesParameterReference.class)
+ SheetName[] m_sheetNames = new SheetName[0];
+
+ @Widget(title = "If sheet exists",
+ description = "Specify the behavior of the node in case a sheet with the entered name already exists. "
+ + "(This option is only relevant if the file append option is selected.)")
+ @ValueSwitchWidget
+ @Persist(configKey = ExcelTableWriterConfig.CFG_SHEET_EXISTS)
+ @Layout(SheetsSection.class)
+ @Effect(predicate = OutputFileOverwritePolicyIsAppend.class, type = Effect.EffectType.SHOW)
+ @ValueReference(SheetExistsPolicyRef.class)
+ @ValueProvider(SheetExistsPolicyFromConcatenateSheetsUpdater.class)
+ SheetExistsPolicy m_ifSheetExists = SheetExistsPolicy.FAIL;
+
+ @Widget(title = "Merge data with identical sheet names into one sheet",
+ description = "If checked, it is possible to provide the same sheet name for multiple input tables. "
+ + "Data from those tables will be concatenated into the same sheet in the order the tables are connected.")
+ @Layout(SheetsSection.class)
+ @Effect(predicate = OutputFileOverwritePolicyIsAppend.class, type = Effect.EffectType.HIDE)
+ @ValueReference(ConcatenateSheetsWithSameNameRef.class)
+ @ValueProvider(ConcatenateSheetsWithSameNameUpdater.class)
+ boolean m_concatenateSheetsWithSameName;
+
+ @TextMessage(SheetNameValidationMessageProvider.class)
+ @Layout(SheetsSection.class)
+ Void m_sheetNameValidationMessage;
+
+ @Widget(title = "Write row key",
+ description = "If checked, the row IDs are added to the output, in the first column of the spreadsheet.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_WRITE_ROW_KEY)
+ @Layout(NamesAndIdsSection.class)
+ boolean m_writeRowKey;
+
+ @Widget(title = "Write column headers",
+ description = "If checked, the column names are written out in the first row of the spreadsheet.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_WRITE_COLUMN_HEADER)
+ @Layout(NamesAndIdsSection.class)
+ boolean m_writeColumnHeaders = true;
+
+ @Widget(title = "Don't write column headers if sheet exists",
+ description = "Only write the column headers if a sheet is newly created or replaced. "
+ + "This option is convenient if you have written data with the same specification to an existing sheet "
+ + "before and want to append new rows to it.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_SKIP_COLUMN_HEADER_ON_APPEND)
+ @Layout(NamesAndIdsSection.class)
+ boolean m_skipColumnHeaderOnAppend = true;
+
+ @Widget(title = "Replace missing values",
+ description = "If selected, missing values will be replaced by the specified value, "
+ + "otherwise a blank cell is being created.")
+ @Persistor(MissingValuePatternPersistor.class)
+ @Migrate
+ @Layout(ValuesSection.class)
+ @TextInputWidget(placeholder = "Replacement value")
+ Optional m_missingValuePattern = Optional.empty();
+
+ @Widget(title = "Evaluate formulas (leave unchecked if uncertain, see help for details)",
+ description = "If checked, all formulas in the file will be evaluated after the sheet has been written. "
+ + "This is useful if other sheets in the file refer to the data just written and their content needs "
+ + "updating. This option is only relevant if the append option is selected. This can cause errors when "
+ + "there are functions that are not implemented by the underlying Apache POI library. "
+ + "Note: For xlsx files, evaluation requires significantly more memory as the whole file needs to be kept "
+ + "in memory (xls files are anyway loaded completely into memory).")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_EVALUATE_FORMULAS)
+ @Layout(ValuesSection.class)
+ boolean m_evaluateFormulas;
+
+ @Widget(title = "Autosize columns", description = "Fits each column's width to its content.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_AUTOSIZE)
+ @Layout(LayoutSection.class)
+ boolean m_autosizeColumns;
+
+ @Widget(title = "Page orientation", description = "Sets the print format to portrait or landscape.")
+ @ValueSwitchWidget
+ @Persist(configKey = ExcelTableWriterConfig.CFG_LANDSCAPE)
+ @Layout(LayoutSection.class)
+ Orientation m_orientation = Orientation.PORTRAIT;
+
+ @Widget(title = "Paper size", description = "Sets the paper size in the print setup.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_PAPER_SIZE)
+ @Layout(LayoutSection.class)
+ PaperSize m_paperSize = PaperSize.A4_PAPERSIZE;
+
+ @Layout(InteractionSection.class)
+ @Widget(title = "Open file after execution",
+ description = "If enabled, the output file will be opened in the associated application "
+ + "after the node has successfully executed.")
+ @Persist(configKey = ExcelTableWriterConfig.CFG_OPEN_FILE_AFTER_EXEC)
+ boolean m_openOutputFileAfterExecution;
+
+ private static final class OutputFileModification implements LegacyFileWriterWithOverwritePolicyOptions.Modifier {
+ private static final class ExcelOverwritePolicyChoicesProvider
+ extends LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicyChoicesProvider {
+
+ @Override
+ protected List getChoices() {
+ return List.of(LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.fail,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.overwrite,
+ LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.append);
+ }
+ }
+
+ @Override
+ public void modify(final Modification.WidgetGroupModifier group) {
+ restrictOverwritePolicyOptions(group, ExcelOverwritePolicyChoicesProvider.class);
+ var fileSelection = findFileSelection(group);
+ fileSelection //
+ .addAnnotation(ValueReference.class) //
+ .withValue(OutputFileSelectionRef.class) //
+ .modify();
+ fileSelection //
+ .addAnnotation(ValueProvider.class) //
+ .withValue(OutputLocationExtensionChanger.class) //
+ .modify();
+ fileSelection //
+ .modifyAnnotation(FileWriterWidget.class) //
+ .withProperty("fileExtensionProvider", OutputLocationFileExtensionProvider.class) //
+ .modify();
+ findOverwritePolicy(group) //
+ .addAnnotation(ValueReference.class) //
+ .withValue(OutputFileOverwritePolicyRef.class) //
+ .modify();
+ }
+ }
+
+ private static class OutputFileSelectionRef implements ParameterReference {
+ }
+
+ private static class OutputFileOverwritePolicyRef
+ implements ParameterReference {
+ }
+
+ private static final class OutputFileRef implements ParameterReference {
+ }
+
+ private static final class OutputFileOverwritePolicyIsAppend implements EffectPredicateProvider {
+ @Override
+ public EffectPredicate init(final PredicateInitializer i) {
+ return ((LegacyPredicateInitializer)i).getLegacyFileWriter(OutputFileRef.class).getOverwritePolicy()
+ .isOneOf(LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.append);
+ }
+ }
+
+ private static final class OutputLocationExtensionChanger implements StateProvider {
+
+ Supplier m_format;
+
+ Supplier m_outputLocation;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ m_format = initializer.computeFromValueSupplier(ExcelFormatRef.class);
+ m_outputLocation = initializer.getValueSupplier(OutputFileSelectionRef.class);
+ }
+
+ @Override
+ public FileSelection computeState(final NodeParametersInput parametersInput)
+ throws StateComputationFailureException {
+ final var oldOutputLocation = m_outputLocation.get();
+ final var oldFsLocation = oldOutputLocation.getFSLocation();
+ final var oldPath = oldFsLocation.getPath();
+ final var format = m_format.get();
+ final var newExtension = "." + format.name().toLowerCase(Locale.ROOT);
+
+ // if path ends with any of the known extensions, replace it
+ for (final var knownFormat : ExcelFormat.values()) {
+ final var knownExtension = "." + knownFormat.name().toLowerCase(Locale.ROOT);
+ if (oldPath.endsWith(knownExtension)) {
+ final var newPath = oldPath.substring(0, oldPath.length() - knownExtension.length()) + newExtension;
+ final var newLocation = new FSLocation(oldFsLocation.getFSCategory(),
+ oldFsLocation.getFileSystemSpecifier().orElse(null), newPath);
+ return new FileSelection(newLocation);
+ }
+ }
+ throw new StateComputationFailureException();
+ }
+ }
+
+ private static final class OutputLocationFileExtensionProvider implements StateProvider {
+
+ Supplier m_format;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ initializer.computeBeforeOpenDialog();
+ m_format = initializer.computeFromValueSupplier(ExcelFormatRef.class);
+ }
+
+ @Override
+ public String computeState(final NodeParametersInput parametersInput) throws StateComputationFailureException {
+ final var format = m_format.get();
+ return format.name().toLowerCase(Locale.ROOT);
+ }
+ }
+
+ private static final class SheetNamesParameterReference implements ParameterReference {
+ }
+
+ private static class SheetName implements NodeParameters {
+
+ SheetName() {
+ // Default constructor
+ }
+
+ SheetName(final String sheetName) {
+ m_sheetName = sheetName;
+ }
+
+ @Widget(title = "Sheet name",
+ description = "Name of the spreadsheets that will be created. The dropdown can be used to select a sheet "
+ + "name which already exists in the Excel file or a custom name can be entered. If \"If sheet exists\" "
+ + "isn't set to append, each sheet name must be unique. The node appends the tables in the order they "
+ + "are connected.")
+ @SuggestionsProvider(SheetNamesChoicesProvider.class)
+ String m_sheetName = "Sheet1";
+ }
+
+ static class SheetNamesDefaultProvider implements StateProvider {
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ // Nothing to initialize
+ }
+
+ @Override
+ public String[] computeState(final NodeParametersInput context) {
+ final int numSheets = context.getPortsConfiguration().getInputPortLocation()
+ .get(ExcelTableWriterNodeFactory.SHEET_GRP_ID).length;
+ return IntStream.range(0, numSheets).mapToObj(ExcelTableWriterConfig::createDefaultSheetName)
+ .toArray(String[]::new);
+ }
+ }
+
+ private static class SheetNamesPersistor implements NodeParametersPersistor {
+
+ @Override
+ public SheetName[] load(final NodeSettingsRO settings) throws InvalidSettingsException {
+ var sheetNames = settings.getStringArray(ExcelTableWriterConfig.CFG_SHEET_NAMES);
+ var result = new SheetName[sheetNames.length];
+ for (var i = 0; i < sheetNames.length; i++) {
+ result[i] = new SheetName(sheetNames[i]);
+ }
+
+ return result;
+ }
+
+ @Override
+ public void save(final SheetName[] param, final NodeSettingsWO settings) {
+ var sheetNames = new String[param.length];
+ for (var i = 0; i < param.length; i++) {
+ sheetNames[i] = param[i].m_sheetName;
+ }
+ settings.addStringArray(ExcelTableWriterConfig.CFG_SHEET_NAMES, sheetNames);
+ }
+
+ @Override
+ public String[][] getConfigPaths() {
+ return new String[][]{{ExcelTableWriterConfig.CFG_SHEET_NAMES}};
+ }
+
+ }
+
+ private static class SheetNamesValueProvider extends PerPortValueProvider {
+
+ SheetNamesValueProvider() {
+ super(ExcelTableWriterNodeFactory.SHEET_GRP_ID, PortGroupSide.INPUT);
+ }
+
+ @Override
+ protected Supplier supplier(final StateProviderInitializer initializer) {
+ return initializer.getValueSupplier(SheetNamesParameterReference.class);
+ }
+
+ @Override
+ protected SheetName[] newArray(final int size) {
+ return new SheetName[size];
+ }
+
+ @Override
+ protected SheetName newInstance() {
+ return new SheetName();
+ }
+ }
+
+ private static class SheetNamesChoicesProvider implements StringChoicesProvider {
+ Supplier m_outputFileSelection;
+
+ Supplier m_encryption;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ /*
+ * Looking up sheet names can take time, especially for large files or remote file systems
+ * -> do it after opening the dialog (otherwise the dialog opening is blocked)
+ */
+ initializer.computeAfterOpenDialog();
+ m_outputFileSelection = initializer.computeFromValueSupplier(OutputFileSelectionRef.class);
+ m_encryption = initializer.computeFromValueSupplier(EncryptionRef.class);
+ }
+
+ @Override
+ public List choices(final NodeParametersInput context) {
+ var fsConnection = FileSystemPortConnectionUtil.getFileSystemConnection(context);
+ try (final var accessor = new FileChooserPathAccessor(m_outputFileSelection.get(), fsConnection)) {
+ final var path = accessor.getPaths(s -> {
+ }).get(0);
+ if (!Files.exists(path)) {
+ return Collections.emptyList();
+ }
+ try (final var inputStream = FSFiles.newInputStream(path)) {
+ final var password = m_encryption.get().getPassword(context);
+ return ExcelUtils.readSheetNames(inputStream, password);
+ }
+ } catch (final Exception e) {
+ // in case of any error, return no choices
+ return Collections.emptyList();
+ }
+ }
+ }
+
+ private static class EncryptionValidationMessageProvider implements SimpleTextMessageProvider {
+
+ Supplier m_outputFileSelection;
+
+ Supplier m_encryption;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ initializer.computeBeforeOpenDialog();
+ m_outputFileSelection = initializer.computeFromValueSupplier(OutputFileSelectionRef.class);
+ m_encryption = initializer.computeFromValueSupplier(EncryptionRef.class);
+ }
+
+ @Override
+ public boolean showMessage(final NodeParametersInput context) {
+ var fsConnection = FileSystemPortConnectionUtil.getFileSystemConnection(context);
+ try (final var accessor = new FileChooserPathAccessor(m_outputFileSelection.get(), fsConnection)) {
+ final var path = accessor.getPaths(s -> {
+ }).get(0);
+ if (!Files.exists(path)) {
+ return false;
+ }
+ try (final var inputStream = FSFiles.newInputStream(path)) {
+ final var password = m_encryption.get().getPassword(context);
+ CryptUtil.verifyPassword(inputStream, password);
+ return false;
+ } catch (final EncryptedDocumentException e) {
+ return true;
+ }
+ } catch (final Exception e) {
+ // in case of any error, do not show the message
+ return false;
+ }
+ }
+
+ @Override
+ public String title() {
+ return "Invalid password";
+ }
+
+ @Override
+ public String description() {
+ return "The provided password is not valid for the selected file. Please provide a correct password"
+ + " via the \"Encryption\" settings.";
+ }
+
+ @Override
+ public MessageType type() {
+ return MessageType.INFO;
+ }
+ }
+
+ private static class SheetNameValidationMessageProvider implements SimpleTextMessageProvider {
+
+ Supplier m_overwritePolicy;
+
+ Supplier m_sheetNames;
+
+ Supplier m_ifSheetExists;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ initializer.computeBeforeOpenDialog();
+ m_overwritePolicy = initializer.computeFromValueSupplier(OutputFileOverwritePolicyRef.class);
+ m_sheetNames = initializer.computeFromValueSupplier(SheetNamesParameterReference.class);
+ m_ifSheetExists = initializer.computeFromValueSupplier(SheetExistsPolicyRef.class);
+ }
+
+ @Override
+ public boolean showMessage(final NodeParametersInput context) {
+ if (m_ifSheetExists.get() == SheetExistsPolicy.APPEND) {
+ return false;
+ }
+ var sheetNames = m_sheetNames.get();
+ var nameSet = Collections.newSetFromMap(new java.util.HashMap());
+ for (var sheetName : sheetNames) {
+ if (!nameSet.add(sheetName.m_sheetName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String title() {
+ return "Duplicate sheet names";
+ }
+
+ @Override
+ public String description() {
+ if (m_overwritePolicy.get() == LegacyFileWriterWithOverwritePolicyOptions.OverwritePolicy.append) {
+ return "Please rename the sheets to have unique names or set the \"If sheet exists\" option to "
+ + "\"Append\" to allow duplicate sheet names.";
+ } else {
+ return "Please rename the sheets to have unique names or enable the \"Merge data with identical sheet "
+ + "names into one sheet\" option to allow duplicate sheet names.";
+ }
+ }
+
+ @Override
+ public MessageType type() {
+ return MessageType.INFO;
+ }
+ }
+
+ private static class ConcatenateSheetsWithSameNameRef implements ParameterReference {
+ }
+
+ private static class SheetExistsPolicyFromConcatenateSheetsUpdater implements StateProvider {
+
+ Supplier m_concatenateSheetsWithSameName;
+
+ Supplier m_ifSheetExists;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ m_concatenateSheetsWithSameName =
+ initializer.computeFromValueSupplier(ConcatenateSheetsWithSameNameRef.class);
+ m_ifSheetExists = initializer.getValueSupplier(SheetExistsPolicyRef.class);
+ }
+
+ @Override
+ public SheetExistsPolicy computeState(final NodeParametersInput parametersInput)
+ throws StateComputationFailureException {
+ if (Boolean.TRUE.equals(m_concatenateSheetsWithSameName.get())) {
+ return SheetExistsPolicy.APPEND;
+ }
+ if (m_ifSheetExists.get() == SheetExistsPolicy.APPEND) {
+ return SheetExistsPolicy.FAIL;
+ }
+ // Do not overwrite the existing value if it is compatible with the current state
+ throw new StateComputationFailureException();
+ }
+ }
+
+ private static class ConcatenateSheetsWithSameNameUpdater implements StateProvider {
+ Supplier m_ifSheetExists;
+
+ @Override
+ public void init(final StateProviderInitializer initializer) {
+ initializer.computeBeforeOpenDialog();
+ initializer.computeOnValueChange(OutputFileRef.class);
+ m_ifSheetExists = initializer.getValueSupplier(SheetExistsPolicyRef.class);
+ }
+
+ @Override
+ public Boolean computeState(final NodeParametersInput parametersInput) {
+ return m_ifSheetExists.get() == SheetExistsPolicy.APPEND;
+ }
+ }
+
+ private static class ExcelFormatRef implements ParameterReference {
+ }
+
+ enum ExcelFormat {
+ @Label(value = "XLSX", //
+ description = "The Office Open XML format is the file format used by default from Excel 2007 onwards. "
+ + "The maximum number of columns and rows held by a spreadsheet of this format is 16384 and "
+ + "1048576 respectively.") //
+ XLSX, //
+ @Label(value = "XLS", //
+ description = "This is the file format which was used by default up until Excel 2003. "
+ + "The maximum number of columns and rows held by a spreadsheet of this format is 256 and 65536 "
+ + "respectively.") //
+ XLS, //
+ ;
+ }
+
+ private static class SheetExistsPolicyRef implements ParameterReference {
+ }
+
+ enum SheetExistsPolicy {
+ @Label(value = "Fail",
+ description = "Will issue an error during the node's execution (to prevent unintentional overwrite).")
+ FAIL, //
+ @Label(value = "Overwrite", description = "Will replace any existing sheet.")
+ OVERWRITE, //
+ @Label(value = "Append",
+ description = "Will append the input tables after the last row of the sheets. "
+ + "Note: the last row is chosen according to the rows which already exist in the sheet. "
+ + "A row may appear empty but still exist because it or one of its cells contains styling "
+ + "information or was not removed by Excel after the user cleared it.")
+ APPEND, //
+ ;
+ }
+
+ private static class MissingValuePatternPersistor implements NodeParametersPersistor> {
+
+ @Override
+ public Optional load(final NodeSettingsRO settings) throws InvalidSettingsException {
+ if (settings.containsKey(ExcelTableWriterConfig.CFG_REPLACE_MISSINGS)
+ && settings.getBoolean(ExcelTableWriterConfig.CFG_REPLACE_MISSINGS)) {
+ String pattern = settings.getString(ExcelTableWriterConfig.CFG_MISSING_VALUE_PATTERN, "");
+ return Optional.of(pattern);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void save(final Optional param, final NodeSettingsWO settings) {
+ if (param.isPresent()) {
+ settings.addBoolean(ExcelTableWriterConfig.CFG_REPLACE_MISSINGS, true);
+ settings.addString(ExcelTableWriterConfig.CFG_MISSING_VALUE_PATTERN, param.get());
+ } else {
+ settings.addBoolean(ExcelTableWriterConfig.CFG_REPLACE_MISSINGS, false);
+ settings.addString(ExcelTableWriterConfig.CFG_MISSING_VALUE_PATTERN, "");
+ }
+ }
+
+ @Override
+ public String[][] getConfigPaths() {
+ return new String[][]{{ExcelTableWriterConfig.CFG_REPLACE_MISSINGS},
+ {ExcelTableWriterConfig.CFG_MISSING_VALUE_PATTERN}};
+ }
+ }
+
+ private static class EncryptionRef implements ParameterReference {
+ }
+}
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/Orientation.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/Orientation.java
index a6980b76..4240ed34 100644
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/Orientation.java
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/Orientation.java
@@ -49,6 +49,7 @@
package org.knime.ext.poi3.node.io.filehandling.excel.writer.util;
import org.knime.core.node.util.ButtonGroupEnumInterface;
+import org.knime.node.parameters.widget.choices.Label;
/**
* Enum encoding the supported orientations by the 'Excel table writer' node.
@@ -58,9 +59,11 @@
public enum Orientation implements ButtonGroupEnumInterface {
/** Portrait orientation. */
+ @Label("Portrait")
PORTRAIT("Portrait"),
/** Landscape orientation. */
+ @Label("Landscape")
LANDSCAPE("Landscape");
private final String m_text;
diff --git a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/PaperSize.java b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/PaperSize.java
index 51d4ce28..b0343050 100644
--- a/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/PaperSize.java
+++ b/org.knime.ext.poi3/src/org/knime/ext/poi3/node/io/filehandling/excel/writer/util/PaperSize.java
@@ -49,6 +49,7 @@
package org.knime.ext.poi3.node.io.filehandling.excel.writer.util;
import org.apache.poi.ss.usermodel.PrintSetup;
+import org.knime.node.parameters.widget.choices.Label;
/**
* Enum encoding the supported paper sizes by the 'Excel table writer' node.
@@ -58,30 +59,39 @@
public enum PaperSize {
/** A4 paper size. */
+ @Label("A4 - 210x297 mm")
A4_PAPERSIZE("A4 - 210x297 mm", PrintSetup.A4_PAPERSIZE),
/** A5 paper size. */
+ @Label("A5 - 148x210 mm")
A5_PAPERSIZE("A5 - 148x210 mm", PrintSetup.A5_PAPERSIZE),
/** Envelope 10 paper size. */
+ @Label("US Envelope #10 4 1/8 x 9 1/2")
ENVELOPE_10_PAPERSIZE("US Envelope #10 4 1/8 x 9 1/2", PrintSetup.ENVELOPE_10_PAPERSIZE),
/** Envelope CS paper size. */
+ @Label("Envelope C5 162x229 mm")
ENVELOPE_CS_PAPERSIZE("Envelope C5 162x229 mm", PrintSetup.ENVELOPE_CS_PAPERSIZE),
/** Envelope DL paper size. */
+ @Label("Envelope DL 110x220 mm")
ENVELOPE_DL_PAPERSIZE("Envelope DL 110x220 mm", PrintSetup.ENVELOPE_DL_PAPERSIZE),
/** Envelope monarch paper size. */
+ @Label("Envelope Monarch 98.4×190.5 mm")
ENVELOPE_MONARCH_PAPERSIZE("Envelope Monarch 98.4×190.5 mm", PrintSetup.ENVELOPE_MONARCH_PAPERSIZE),
/** Executive paper size. */
+ @Label("US Executive 7 1/4 x 10 1/2 in")
EXECUTIVE_PAPERSIZE("US Executive 7 1/4 x 10 1/2 in", PrintSetup.EXECUTIVE_PAPERSIZE),
/** Legal paper size. */
+ @Label("US Legal 8 1/2 x 14 in")
LEGAL_PAPERSIZE("US Legal 8 1/2 x 14 in", PrintSetup.LEGAL_PAPERSIZE),
/** Letter paper size. */
+ @Label("US Letter 8 1/2 x 11 in")
LETTER_PAPERSIZE("US Letter 8 1/2 x 11 in", PrintSetup.LETTER_PAPERSIZE);
private PaperSize(final String text, final short printSetup) {