/*
* ------------------------------------------------------------------------
*
* Copyright (C) 2003 - 2013
* University of Konstanz, Germany and
* KNIME GmbH, Konstanz, Germany
* Website: http://www.knime.org; Email: contact@knime.org
*
* 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 <http://www.gnu.org/licenses>.
*
* 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 GMBH 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.knip.base.node;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import org.knime.core.data.DataCell;
import org.knime.core.data.DataColumnSpec;
import org.knime.core.data.DataColumnSpecCreator;
import org.knime.core.data.DataRow;
import org.knime.core.data.DataTableSpec;
import org.knime.core.data.DataType;
import org.knime.core.data.DataValue;
import org.knime.core.data.RowIterator;
import org.knime.core.data.RowKey;
import org.knime.core.data.container.CellFactory;
import org.knime.core.data.container.ColumnRearranger;
import org.knime.core.data.def.DefaultRow;
import org.knime.core.node.BufferedDataContainer;
import org.knime.core.node.BufferedDataTable;
import org.knime.core.node.BufferedDataTableHolder;
import org.knime.core.node.CanceledExecutionException;
import org.knime.core.node.ExecutionContext;
import org.knime.core.node.ExecutionMonitor;
import org.knime.core.node.InvalidSettingsException;
import org.knime.core.node.NodeLogger;
import org.knime.core.node.NodeModel;
import org.knime.core.node.NodeSettingsRO;
import org.knime.core.node.NodeSettingsWO;
import org.knime.core.node.defaultnodesettings.SettingsModel;
import org.knime.core.node.defaultnodesettings.SettingsModelString;
import org.knime.core.node.port.PortObject;
import org.knime.core.node.port.PortObjectSpec;
import org.knime.core.node.port.PortType;
import org.knime.core.node.streamable.InputPortRole;
import org.knime.core.node.streamable.OutputPortRole;
import org.knime.core.node.streamable.PartitionInfo;
import org.knime.core.node.streamable.StreamableFunction;
import org.knime.core.node.streamable.StreamableOperator;
import org.knime.core.node.streamable.StreamableOperatorInternals;
import org.knime.core.util.Pair;
import org.knime.knip.base.exceptions.KNIPException;
import org.knime.knip.base.exceptions.KNIPRuntimeException;
import org.knime.knip.base.exceptions.LoggerHelper;
/**
*
* Node Model to process table cells separately.
*
* @param <VIN> the type of the input values
* @author <a href="mailto:dietzc85@googlemail.com">Christian Dietz</a>
* @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a>
* @author <a href="mailto:michael.zinsmaier@googlemail.com">Michael Zinsmaier</a>
*/
public abstract class ValueToCellsNodeModel<VIN extends DataValue> extends NodeModel
implements BufferedDataTableHolder {
/**
* column creation modes
*/
public static final String[] COL_CREATION_MODES = new String[]{"New Table", "Append"};
/*
* Inport of the table to be processed
*/
private static final int IN_TABLE_PORT_INDEX = 0;
/*
* Logging
*/
private static final NodeLogger LOGGER = NodeLogger.getLogger(ValueToCellsNodeModel.class);
/**
* @return the settings model for the column creation mode
*/
public static SettingsModelString createColCreationModeModel() {
return new SettingsModelString("column_creation_mode", COL_CREATION_MODES[0]);
}
/**
* @return the settings model for the column name
*/
public static SettingsModelString createColSelectionModel() {
return new SettingsModelString("column_selection", "");
}
private static PortType[] createInPortType(final PortType[] additionalPorts) {
final PortType[] inPTypes = new PortType[additionalPorts.length + 1];
inPTypes[0] = BufferedDataTable.TYPE;
for (int i = 0; i < additionalPorts.length; i++) {
inPTypes[i + 1] = additionalPorts[i];
}
return inPTypes;
}
/*
* Settings for the column creation mode
*/
private final SettingsModelString m_colCreationMode = createColCreationModeModel();
/*
* Settings Model to store the column to be processed
*/
private final SettingsModelString m_column = createColSelectionModel();
/*
* data table for the table cell view
*/
private BufferedDataTable m_data;
/**
* Class of the first argument type.
*/
protected Class<VIN> m_inValueClass;
/*
*
* Number of occurred errors while processing all available cells
*/
private int m_numOccurredErrors;
/*
* Collection of all settings model used.
*/
private List<SettingsModel> m_settingsModels = null;
/**
*/
protected ValueToCellsNodeModel() {
super(1, 1);
getTypeArgumentClasses();
}
/**
* Contructor to add additional in-ports to the node. The data table port used within this model has index 0! Hence,
* all additionally added ports will have a index > 0.
*
* If you want to overwrite the configure/execute-methods and still want to use the functionality provided by this
* model, then make sure to overwrite the right methods:
*
* {@link #configure(PortObjectSpec[])} and {@link #execute(PortObject[], ExecutionContext)}, and don't forget to
* call super.... .
*
* @param additionalPorts specifies additional ports
*/
protected ValueToCellsNodeModel(final PortType[] additionalPorts) {
super(createInPortType(additionalPorts), new PortType[]{BufferedDataTable.TYPE});
getTypeArgumentClasses();
}
/**
* Adds the settings model to be saved, load and validated.
*
* @param settingsModels
*/
protected abstract void addSettingsModels(List<SettingsModel> settingsModels);
/*
* Helper to collect all settings models and add them to one list (if
* not already done)
*/
private void collectSettingsModels() {
if (m_settingsModels == null) {
m_settingsModels = new ArrayList<SettingsModel>();
m_settingsModels.add(m_colCreationMode);
m_settingsModels.add(m_column);
addSettingsModels(m_settingsModels);
}
}
/**
* Processes one single data cell. Exactly one of the compute-methods has to be overwritten.
*
* @param cellValue
* @return computed {@link DataCell}s
* @throws Exception
*/
protected abstract DataCell[] compute(VIN cellValue) throws Exception;
/**
* {@inheritDoc}
*/
@Override
protected DataTableSpec[] configure(final DataTableSpec[] inSpecs) throws InvalidSettingsException {
final int colIndex = NodeUtils.getColumnIndex(m_column, inSpecs[0], m_inValueClass, this.getClass());
final CellFactory cellFac = createCellFactory(inSpecs[0], colIndex);
ColumnRearranger colRearranger;
if (m_colCreationMode.getStringValue().equals(COL_CREATION_MODES[0])) {
return new DataTableSpec[]{new DataTableSpec(cellFac.getColumnSpecs())};
} else {
colRearranger = new ColumnRearranger(inSpecs[0]);
colRearranger.append(cellFac);
return new DataTableSpec[]{colRearranger.createSpec()};
}
}
/*
* Creates the cell factory.
*/
private CellFactory createCellFactory(final DataTableSpec inSpec, final int colIdx) {
// New spec is created
final Pair<DataType[], String[]> Pair = getDataOutTypeAndName();
final DataType[] dt = Pair.getFirst();
final String[] dataOutTypeNames = Pair.getSecond();
final DataColumnSpec[] cspecs = new DataColumnSpec[dt.length];
for (int i = 0; i < dt.length; i++) {
cspecs[i] = new DataColumnSpecCreator(DataTableSpec.getUniqueColumnName(inSpec, dataOutTypeNames[i]), dt[i]).createSpec();
}
return new CellFactory() {
@SuppressWarnings("unchecked")
@Override
public DataCell[] getCells(final DataRow row) {
DataCell[] cells = null;
try {
if ((row.getCell(colIdx).isMissing())) {
LOGGER.warn("Missing cell was ignored at row " + row.getKey());
m_numOccurredErrors++;
cells = new DataCell[dt.length];
for (int i = 0; i < cells.length; i++) {
cells[i] = DataType.getMissingCell();
}
} else {
cells = compute((VIN)row.getCell(colIdx));
for (int i = 0; i < cells.length; i++) {
if (cells[i] == null) {
LOGGER.warn("Node didn't provide an output at row " + row.getKey()
+ ". Missing cell has been inserted.");
cells[i] = DataType.getMissingCell();
}
}
}
} catch (final Exception e) {
if (e.getCause() instanceof InterruptedException) {
LOGGER.warn("Interrupted node");
} else {
if (e instanceof KNIPException) {
LoggerHelper.log("Problem occured in row" + row.getKey(), LOGGER, (KNIPException)e,
"Missing cell has been inserted");
} else if (e instanceof KNIPRuntimeException) {
LoggerHelper.log("Problem occured in row" + row.getKey(), LOGGER, (KNIPRuntimeException)e,
"Missing cell has been inserted");
} else {
LOGGER.error("Error in row " + row.getKey() + ": " + e.getMessage(), e);
throw new RuntimeException(e);
}
m_numOccurredErrors++;
}
cells = new DataCell[dt.length];
for (int i = 0; i < cells.length; i++) {
cells[i] = DataType.getMissingCell();
}
}
return cells;
}
@Override
public DataColumnSpec[] getColumnSpecs() {
return cspecs;
}
@Override
public void setProgress(final int curRowNr, final int rowCount, final RowKey lastKey,
final ExecutionMonitor exec) {
exec.setProgress((double)curRowNr / rowCount);
}
};
}
/**
* {@inheritDoc}
*/
@Override
protected BufferedDataTable[] execute(final BufferedDataTable[] inObjects, final ExecutionContext exec)
throws Exception {
final BufferedDataTable inTable = inObjects[IN_TABLE_PORT_INDEX];
exec.setProgress("Initializing ...");
prepareExecute(exec);
m_numOccurredErrors = 0;
BufferedDataTable[] res;
final int selectedColIndices =
NodeUtils.getColumnIndex(m_column, inTable.getDataTableSpec(), m_inValueClass, this.getClass());
final CellFactory cellFac = createCellFactory(inObjects[0].getDataTableSpec(), selectedColIndices);
exec.setProgress("Processing ...");
if (m_colCreationMode.getStringValue().equals(COL_CREATION_MODES[0])) {
final RowIterator it = inTable.iterator();
final BufferedDataContainer con = exec.createDataContainer(new DataTableSpec(cellFac.getColumnSpecs()));
DataRow row;
final int rowCount = inTable.getRowCount();
int i = 0;
while (it.hasNext()) {
row = it.next();
con.addRowToTable(new DefaultRow(row.getKey(), cellFac.getCells(row)));
exec.checkCanceled();
cellFac.setProgress(i, rowCount, row.getKey(), exec);
i++;
}
con.close();
res = new BufferedDataTable[]{con.getTable()};
} else {
final ColumnRearranger colRearranger = new ColumnRearranger(inTable.getDataTableSpec());
if (m_colCreationMode.getStringValue().equals(COL_CREATION_MODES[1])) {
colRearranger.append(cellFac);
} else {
colRearranger.replace(cellFac, selectedColIndices);
}
res = new BufferedDataTable[]{exec.createColumnRearrangeTable(inTable, colRearranger, exec)};
}
if (m_numOccurredErrors > 0) {
setWarningMessage(m_numOccurredErrors + " errors occurred while executing! See console log for details!");
}
// data for the table cell view
m_data = res[0];
return res;
}
/**
*
* Return the DataTypes and Names of the new cells
*
* @return the data out types
*/
protected abstract Pair<DataType[], String[]> getDataOutTypeAndName();
/**
* {@inheritDoc}
*/
@Override
public BufferedDataTable[] getInternalTables() {
return new BufferedDataTable[]{m_data};
}
/**
* Retrieves the classes of the type arguments VIN and COUT.
*/
@SuppressWarnings("unchecked")
protected void getTypeArgumentClasses() {
// TODO: is there a better way??
Class<?> c = getClass();
for (int i = 0; i < 5; i++) {
if (c.getSuperclass().equals(ValueToCellsNodeModel.class)) {
final Type[] types = ((ParameterizedType)c.getGenericSuperclass()).getActualTypeArguments();
if (types[0] instanceof ParameterizedType) {
types[0] = ((ParameterizedType)types[0]).getRawType();
}
m_inValueClass = (Class<VIN>)types[0];
break;
}
c = c.getSuperclass();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void loadInternals(final File nodeInternDir, final ExecutionMonitor exec)
throws IOException, CanceledExecutionException {
//
}
/**
* {@inheritDoc}
*/
@Override
protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws InvalidSettingsException {
collectSettingsModels();
for (final SettingsModel sm : m_settingsModels) {
sm.loadSettingsFrom(settings);
}
}
/**
* Will be called before calling the {@link ValueToCellsNodeModel#compute(DataValue)} multiple times. Has to be
* overwritten if needed.
*
* @param exec the current {@link ExecutionContext}
*/
protected void prepareExecute(final ExecutionContext exec) {
//
}
/**
* {@inheritDoc}
*/
@Override
protected void reset() {
m_data = null;
}
/**
* {@inheritDoc}
*/
@Override
protected void saveInternals(final File nodeInternDir, final ExecutionMonitor exec)
throws IOException, CanceledExecutionException {
//
}
/**
* {@inheritDoc}
*/
@Override
protected void saveSettingsTo(final NodeSettingsWO settings) {
collectSettingsModels();
for (final SettingsModel sm : m_settingsModels) {
sm.saveSettingsTo(settings);
}
}
// // Methods for the table cell view ////
/**
* {@inheritDoc}
*/
@Override
public void setInternalTables(final BufferedDataTable[] tables) {
m_data = tables[0];
}
/**
* {@inheritDoc}
*/
@Override
protected void validateSettings(final NodeSettingsRO settings) throws InvalidSettingsException {
collectSettingsModels();
for (final SettingsModel sm : m_settingsModels) {
try {
sm.validateSettings(settings);
} catch (final InvalidSettingsException e) {
LOGGER.warn("Problems occurred validating the settings " + sm.toString() + ": "
+ e.getLocalizedMessage());
setWarningMessage("Problems occurred while validating settings.");
}
}
}
/*
* STREAMING
*/
@Override
public InputPortRole[] getInputPortRoles() {
return new InputPortRole[]{InputPortRole.DISTRIBUTED_STREAMABLE};
}
@Override
public OutputPortRole[] getOutputPortRoles() {
return new OutputPortRole[]{OutputPortRole.DISTRIBUTED};
}
@Override
public StreamableOperator createStreamableOperator(final PartitionInfo partitionInfo,
final PortObjectSpec[] inSpecs) throws InvalidSettingsException {
final DataTableSpec inSpec = (DataTableSpec)inSpecs[IN_TABLE_PORT_INDEX];
final int colIndex = NodeUtils.getColumnIndex(m_column, inSpec, m_inValueClass, this.getClass());
final CellFactory cellFac = createCellFactory(inSpec, colIndex);
if (m_colCreationMode.getStringValue().equals(COL_CREATION_MODES[0])) {
return new StreamableFunction() {
/**
* {@inheritDoc}
*/
@Override
public void init(final ExecutionContext ctx) throws Exception {
prepareExecute(ctx);
}
@Override
public DataRow compute(final DataRow input) throws Exception {
return new DefaultRow(input.getKey(), cellFac.getCells(input));
}
};
}
// create column rearranger
final ColumnRearranger colRearranger = new ColumnRearranger(inSpec);
if (m_colCreationMode.getStringValue().equals(COL_CREATION_MODES[1])) {
colRearranger.append(cellFac);
}
// get column rearranger function
final StreamableFunction columnRearrangerFunction = colRearranger.createStreamableFunction();
// create new streamablefunction, do everything columnrearranger function does but call prepareexceute in init().
return new StreamableFunction() {
/** {@inheritDoc} */
@Override
public void init(final ExecutionContext ctx) throws Exception {
prepareExecute(ctx);
columnRearrangerFunction.init(ctx);
}
/** {@inheritDoc} */
@Override
public DataRow compute(final DataRow inputRow) {
try {
return columnRearrangerFunction.compute(inputRow);
} catch (Exception e) {
throw new IllegalArgumentException("Exception caught while reading row " + inputRow.getKey()
+ "! Caught exception " + e.getMessage());
}
}
/** {@inheritDoc} */
@Override
public void finish() {
columnRearrangerFunction.finish();
}
/** {@inheritDoc} */
@Override
public StreamableOperatorInternals saveInternals() {
return columnRearrangerFunction.saveInternals();
}
};
}
}