org.pentaho.di.ui.trans.step.BaseStepDialog.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.di.ui.trans.step.BaseStepDialog.java

Source

/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2018 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.ui.trans.step;

import com.google.common.annotations.VisibleForTesting;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.SourceToTargetMapping;
import org.pentaho.di.core.database.DatabaseInterface;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.logging.LoggingObjectType;
import org.pentaho.di.core.logging.SimpleLoggingObject;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.StepPluginType;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.variables.VariableSpace;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.laf.BasePropertyHandler;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.repository.RepositoryElementMetaInterface;
import org.pentaho.di.shared.SharedObjects;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.BaseStepMeta;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.step.StepMetaInterface;
import org.pentaho.di.ui.core.ConstUI;
import org.pentaho.di.ui.core.PropsUI;
import org.pentaho.di.ui.core.database.dialog.DatabaseDialog;
import org.pentaho.di.ui.core.database.wizard.CreateDatabaseWizard;
import org.pentaho.di.ui.core.dialog.EnterMappingDialog;
import org.pentaho.di.ui.core.dialog.ErrorDialog;
import org.pentaho.di.ui.core.gui.GUIResource;
import org.pentaho.di.ui.core.gui.WindowProperty;
import org.pentaho.di.ui.core.widget.ComboVar;
import org.pentaho.di.ui.core.widget.TableView;
import org.pentaho.di.ui.util.DialogUtils;
import org.pentaho.di.ui.util.HelpUtils;
import org.pentaho.metastore.api.IMetaStore;

import java.util.ArrayList;
import java.util.List;

/**
 * This class provides functionality common to Step Dialogs.
 */
public class BaseStepDialog extends Dialog {

    /**
     * The package name used for internationalization
     */
    private static Class<?> PKG = StepInterface.class; // for i18n purposes, needed by Translator2!!

    /**
     * The logging object interface for this dialog.
     */
    public static final LoggingObjectInterface loggingObject = new SimpleLoggingObject("Step dialog",
            LoggingObjectType.STEPDIALOG, null);

    /**
     * The variable bindings for this dialog.
     */
    protected static VariableSpace variables = new Variables();

    /**
     * The step name.
     */
    protected String stepname;

    /**
     * The Step name label.
     */
    protected Label wlStepname;

    /**
     * The Step name UI component.
     */
    protected Text wStepname;

    /**
     * The FormData for the step name and its label.
     */
    protected FormData fdlStepname, fdStepname;

    /**
     * Common dialog buttons.
     */
    protected Button wOK, wGet, wPreview, wSQL, wCreate, wCancel;

    /**
     * FormData for the common dialog buttons.
     */
    protected FormData fdOK, fdGet, fdPreview, fdSQL, fdCreate, fdCancel;

    /**
     * Listeners for the common dialog buttons.
     */
    protected Listener lsOK, lsGet, lsPreview, lsSQL, lsCreate, lsCancel;

    /**
     * The metadata for the associated transformation.
     */
    protected TransMeta transMeta;

    /**
     * A reference to the shell.
     */
    protected Shell shell;

    /**
     * A listener adapter for default widget selection.
     */
    protected SelectionAdapter lsDef;

    /**
     * A listener for dialog resizing.
     */
    protected Listener lsResize;

    /**
     * Whether the dialog (and its backup) have changed.
     */
    protected boolean changed, backupChanged;

    /**
     * The base step meta.
     */
    protected StepMetaInterface baseStepMeta;

    /**
     * The UI properties.
     */
    protected PropsUI props;

    /**
     * The associated repository.
     */
    protected Repository repository;

    /**
     * The MetaStore to use
     */
    protected IMetaStore metaStore;

    /**
     * The step meta for this dialog.
     */
    protected StepMeta stepMeta;

    /**
     * The log channel for this dialog.
     */
    protected LogChannel log;

    /**
     * A constant indicating a center button alignment.
     */
    protected static final int BUTTON_ALIGNMENT_CENTER = 0;

    /**
     * A constant indicating a left button alignment.
     */
    protected static final int BUTTON_ALIGNMENT_LEFT = 1;

    /**
     * A constant indicating a right button alignment.
     */
    public static final int BUTTON_ALIGNMENT_RIGHT = 2;

    /**
     * The button alignment (defaults to center).
     */
    protected static int buttonAlignment = BUTTON_ALIGNMENT_CENTER;

    /**
     * A reference to a database dialog.
     */
    protected DatabaseDialog databaseDialog;

    static {
        // Get the button alignment
        buttonAlignment = getButtonAlignment();
    }

    /**
     * Instantiates a new base step dialog.
     *
     * @param parent       the parent shell
     * @param baseStepMeta the associated base step metadata
     * @param transMeta    the associated transformation metadata
     * @param stepname     the step name
     */
    public BaseStepDialog(Shell parent, BaseStepMeta baseStepMeta, TransMeta transMeta, String stepname) {
        super(parent, SWT.NONE);

        this.log = new LogChannel(baseStepMeta);
        this.transMeta = transMeta;
        this.stepname = stepname;
        this.stepMeta = transMeta.findStep(stepname);
        this.baseStepMeta = (StepMetaInterface) baseStepMeta;
        this.backupChanged = baseStepMeta.hasChanged();
        this.props = PropsUI.getInstance();
    }

    /**
     * Instantiates a new base step dialog.
     *
     * @param parent       the parent shell
     * @param baseStepMeta the associated base step metadata
     * @param transMeta    the associated transformation metadata
     * @param stepname     the step name
     */
    public BaseStepDialog(Shell parent, StepMetaInterface baseStepMeta, TransMeta transMeta, String stepname) {
        super(parent, SWT.NONE);

        this.log = new LogChannel(baseStepMeta);
        this.transMeta = transMeta;
        this.stepname = stepname;
        this.stepMeta = transMeta.findStep(stepname);
        this.baseStepMeta = baseStepMeta;
        this.backupChanged = baseStepMeta.hasChanged();
        this.props = PropsUI.getInstance();
    }

    /**
     * Instantiates a new base step dialog.
     *
     * @param parent the parent shell
     * @param nr     the number of rows
     * @param in     the base step metadata
     * @param tr     the transformation metadata
     */
    public BaseStepDialog(Shell parent, int nr, BaseStepMeta in, TransMeta tr) {
        this(parent, in, tr, null);
    }

    /**
     * Sets the shell image.
     *
     * @param shell             the shell
     * @param stepMetaInterface the step meta interface (because of the legacy code)
     */
    public void setShellImage(Shell shell, StepMetaInterface stepMetaInterface) {

        setShellImage(shell);

        if (stepMeta.isDeprecated()) {

            addDeprecation();
        }
    }

    private void addDeprecation() {

        if (shell == null) {

            return;
        }
        shell.addShellListener(new ShellAdapter() {

            private boolean deprecation = false;

            @Override
            public void shellActivated(ShellEvent shellEvent) {
                super.shellActivated(shellEvent);
                if (!stepMeta.isDeprecated() || deprecation) {
                    return;
                }
                String deprecated = BaseMessages.getString(PKG, "BaseStep.Category.Deprecated").toLowerCase();
                shell.setText(shell.getText() + " (" + deprecated + ")");
                deprecation = true;
            }
        });
    }

    /**
     * Dispose this dialog.
     */
    public void dispose() {
        WindowProperty winprop = new WindowProperty(shell);
        props.setScreen(winprop);
        shell.dispose();
    }

    /**
     * Set the shell size, based upon the previous time the geometry was saved in the Properties file.
     */
    public void setSize() {
        setSize(shell);
    }

    /**
     * Sets the button positions.
     *
     * @param buttons     the buttons
     * @param margin      the margin between buttons
     * @param lastControl the last control
     */
    protected void setButtonPositions(Button[] buttons, int margin, Control lastControl) {
        BaseStepDialog.positionBottomButtons(shell, buttons, margin, lastControl);
    }

    /**
     * Position the specified buttons at the bottom of the parent composite. Also, make the buttons all the same width:
     * the width of the largest button.
     * <p/>
     * The default alignment for buttons in the system will be used. This is set as an LAF property with the key
     * <code>Button_Position</code> and has the valid values of <code>left, center, right</code> with <code>center</code>
     * being the default.
     *
     * @param composite   the composite
     * @param buttons     The buttons to position.
     * @param margin      The margin between the buttons in pixels
     * @param lastControl the last control
     */
    public static final void positionBottomButtons(Composite composite, Button[] buttons, int margin,
            Control lastControl) {
        // call positionBottomButtons method the system button alignment
        positionBottomButtons(composite, buttons, margin, buttonAlignment, lastControl);
    }

    public static final void positionBottomRightButtons(Composite composite, Button[] buttons, int margin,
            Control lastControl) {
        positionBottomButtons(composite, buttons, margin, BUTTON_ALIGNMENT_RIGHT, lastControl);
    }

    public static final void positionBottomButtons(Composite composite, Button[] buttons, int margin, int alignment,
            Control lastControl) {
        // Determine the largest button in the array
        Rectangle largest = null;
        for (int i = 0; i < buttons.length; i++) {
            buttons[i].pack(true);
            Rectangle r = buttons[i].getBounds();
            if (largest == null || r.width > largest.width) {
                largest = r;
            }

            // Also, set the tooltip the same as the name if we don't have one...
            if (buttons[i].getToolTipText() == null) {
                buttons[i].setToolTipText(Const.replace(buttons[i].getText(), "&", ""));
            }
        }

        // Make buttons a bit larger... (nicer)
        largest.width += 10;
        if ((largest.width % 2) == 1) {
            largest.width++;
        }

        // Compute the left side of the 1st button
        switch (alignment) {
        case BUTTON_ALIGNMENT_CENTER:
            centerButtons(buttons, largest.width, margin, lastControl);
            break;
        case BUTTON_ALIGNMENT_LEFT:
            leftAlignButtons(buttons, largest.width, margin, lastControl);
            break;
        case BUTTON_ALIGNMENT_RIGHT:
            rightAlignButtons(buttons, largest.width, margin, lastControl);
            break;
        default:
            break;
        }
        if (Const.isOSX()) {
            Shell parentShell = composite.getShell();
            final List<TableView> tableViews = new ArrayList<TableView>();
            getTableViews(parentShell, tableViews);
            for (final Button button : buttons) {
                // We know the table views
                // We also know that if a button is hit, the table loses focus
                // In that case, we can apply the content of an open text editor...
                //
                button.addSelectionListener(new SelectionAdapter() {

                    public void widgetSelected(SelectionEvent e) {
                        for (TableView view : tableViews) {
                            view.applyOSXChanges();
                        }
                    }
                });
            }
        }
    }

    /**
     * Gets the table views.
     *
     * @param parentControl the parent control
     * @param tableViews    the table views
     * @return the table views
     */
    private static final void getTableViews(Control parentControl, List<TableView> tableViews) {
        if (parentControl instanceof TableView) {
            tableViews.add((TableView) parentControl);
        } else {
            if (parentControl instanceof Composite) {
                Control[] children = ((Composite) parentControl).getChildren();
                for (Control child : children) {
                    getTableViews(child, tableViews);
                }
            } else {
                if (parentControl instanceof Shell) {
                    Control[] children = ((Shell) parentControl).getChildren();
                    for (Control child : children) {
                        getTableViews(child, tableViews);
                    }

                }
            }
        }
    }

    /**
     * Returns the default alignment for the buttons. This is set in the LAF properties with the key
     * <code>Button_Position</code>. The valid values are: <UL> <LI><code>left</code> <LI><code>center</code>
     * <LI><code>right</code> </UL> NOTE: if the alignment is not provided or contains an invalid value,
     * <code>center</code> will be used as a default
     *
     * @return a constant which indicates the button alignment
     */
    protected static int getButtonAlignment() {
        String buttonAlign = BasePropertyHandler.getProperty("Button_Position", "center").toLowerCase();
        if ("center".equals(buttonAlign)) {
            return BUTTON_ALIGNMENT_CENTER;
        } else if ("left".equals(buttonAlign)) {
            return BUTTON_ALIGNMENT_LEFT;
        } else {
            return BUTTON_ALIGNMENT_RIGHT;
        }
    }

    /**
     * Creats a default FormData object with the top / bottom / and left set (this is done to cut down on repetative code
     * lines.
     *
     * @param button      the button to which this form data will be applied
     * @param width       the width of the button
     * @param margin      the margin between buttons
     * @param lastControl the last control above the buttons
     * @return the newly created FormData object
     */
    private static FormData createDefaultFormData(Button button, int width, int margin, Control lastControl) {
        FormData formData = new FormData();
        if (lastControl != null) {
            formData.top = new FormAttachment(lastControl, margin * 3);
        } else {
            formData.bottom = new FormAttachment(100, 0);
        }
        formData.right = new FormAttachment(button, width + margin);
        return formData;
    }

    /**
     * Aligns the buttons as left-aligned on the dialog.
     *
     * @param buttons     the array of buttons to align
     * @param width       the standardized width of all the buttons
     * @param margin      the margin between buttons
     * @param lastControl (optional) the bottom most control used for aligning the buttons relative to the bottom of the
     *                    controls on the dialog
     */
    protected static void leftAlignButtons(Button[] buttons, int width, int margin, Control lastControl) {
        for (int i = 0; i < buttons.length; ++i) {
            FormData formData = createDefaultFormData(buttons[i], width, margin, lastControl);

            // Set the left side of the buttons (either offset from the edge, or relative to the previous button)
            if (i == 0) {
                formData.left = new FormAttachment(0, margin);
            } else {
                formData.left = new FormAttachment(buttons[i - 1], margin);
            }

            // Apply the layout data
            buttons[i].setLayoutData(formData);
        }
    }

    /**
     * Aligns the buttons as right-aligned on the dialog.
     *
     * @param buttons     the array of buttons to align
     * @param width       the standardized width of all the buttons
     * @param margin      the margin between buttons
     * @param lastControl (optional) the bottom most control used for aligning the buttons relative to the bottom of the
     *                    controls on the dialog
     */
    protected static void rightAlignButtons(Button[] buttons, int width, int margin, Control lastControl) {
        for (int i = buttons.length - 1; i >= 0; --i) {
            FormData formData = createDefaultFormData(buttons[i], width, margin, lastControl);

            // Set the right side of the buttons (either offset from the edge, or relative to the previous button)
            if (i == buttons.length - 1) {
                formData.left = new FormAttachment(100, -(width + margin));
            } else {
                formData.left = new FormAttachment(buttons[i + 1], -(2 * (width + margin)) - margin);
            }

            // Apply the layout data
            buttons[i].setLayoutData(formData);
        }
    }

    /**
     * Aligns the buttons as centered on the dialog.
     *
     * @param buttons     the array of buttons to align
     * @param width       the standardized width of all the buttons
     * @param margin      the margin between buttons
     * @param lastControl (optional) the bottom most control used for aligning the buttons relative to the bottom of the
     *                    controls on the dialog
     */
    protected static void centerButtons(Button[] buttons, int width, int margin, Control lastControl) {
        // Setup the middle button
        int middleButtonIndex = buttons.length / 2;
        FormData formData = createDefaultFormData(buttons[middleButtonIndex], width, margin, lastControl);

        // See if we have an even or odd number of buttons...
        int leftOffset = 0;
        if (buttons.length % 2 == 0) {
            // Even number of buttons - the middle is between buttons. The "middle" button is
            // actually to the right of middle
            leftOffset = margin;
        } else {
            // Odd number of buttons - tht middle is in the middle of the button
            leftOffset = -(width + margin) / 2;
        }
        formData.left = new FormAttachment(50, leftOffset);
        buttons[middleButtonIndex].setLayoutData(formData);

        // Do the buttons to the right of the middle
        for (int i = middleButtonIndex + 1; i < buttons.length; ++i) {
            formData = createDefaultFormData(buttons[i], width, margin, lastControl);
            formData.left = new FormAttachment(buttons[i - 1], margin);
            buttons[i].setLayoutData(formData);
        }

        // Do the buttons to the left of the middle
        for (int i = middleButtonIndex - 1; i >= 0; --i) {
            formData = createDefaultFormData(buttons[i], width, margin, lastControl);
            formData.left = new FormAttachment(buttons[i + 1], -(2 * (width + margin)) - margin);
            buttons[i].setLayoutData(formData);
        }
    }

    /**
     * Gets the modify listener tooltip text.
     *
     * @param textField the text field
     * @return the modify listener tooltip text
     */
    public static final ModifyListener getModifyListenerTooltipText(final Text textField) {
        return new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                // maybe replace this with extra arguments
                textField.setToolTipText(variables.environmentSubstitute(textField.getText()));
            }
        };
    }

    /**
     * Adds the databases to the Combo Box component.
     *
     * @param wConnection the Combo Box component
     */
    public void addDatabases(CCombo wConnection) {
        addDatabases(wConnection, null);
    }

    /**
     * Adds the databases with the specified type to the Combo Box component.
     *
     * @param wConnection  the Combo Box component
     * @param databaseType the database type
     */
    public void addDatabases(CCombo wConnection, Class<? extends DatabaseInterface> databaseType) {
        for (int i = 0; i < transMeta.nrDatabases(); i++) {
            DatabaseMeta ci = transMeta.getDatabase(i);
            if (databaseType == null || ci.getDatabaseInterface().getClass().equals(databaseType)) {
                wConnection.add(ci.getName());
            }
        }
    }

    /**
     * Selects the database with the specified name in the Combo Box component.
     *
     * @param wConnection the Combo Box component
     * @param name        the name of the database to select
     */
    public void selectDatabase(CCombo wConnection, String name) {
        int idx = wConnection.indexOf(name);
        if (idx >= 0) {
            wConnection.select(idx);
        }
    }

    /**
     * Adds the connection line.
     *
     * @param parent   the parent UI component
     * @param previous the previous UI component
     * @param middle   the middle
     * @param margin   the margin
     * @return the the Combo Box component for the given parameters
     */
    public CCombo addConnectionLine(Composite parent, Control previous, int middle, int margin) {
        return addConnectionLine(parent, previous, middle, margin, null);
    }

    /**
     * Adds the connection line.
     *
     * @param parent       the parent UI component
     * @param previous     the previous UI component
     * @param middle       the middle
     * @param margin       the margin
     * @param databaseType the database type
     * @return the Combo Box component for the given parameters
     */
    public CCombo addConnectionLine(Composite parent, Control previous, int middle, int margin,
            Class<? extends DatabaseInterface> databaseType) {
        return addConnectionLine(parent, previous, middle, margin, new Label(parent, SWT.RIGHT),
                new Button(parent, SWT.PUSH), new Button(parent, SWT.PUSH), new Button(parent, SWT.PUSH),
                databaseType);
    }

    /**
     * Adds the connection line.
     *
     * @param parent        the parent UI component
     * @param previous      the previous UI component
     * @param middle        the middle
     * @param margin        the margin
     * @param wlConnection  the connection label
     * @param wbnConnection the "new connection" button
     * @param wbeConnection the "edit connection" button
     * @return the Combo Box component for the given parameters
     */
    public CCombo addConnectionLine(Composite parent, Control previous, int middle, int margin,
            final Label wlConnection, final Button wbwConnection, final Button wbnConnection,
            final Button wbeConnection) {
        return addConnectionLine(parent, previous, middle, margin, wlConnection, wbwConnection, wbnConnection,
                wbeConnection, null);
    }

    /**
     * Adds the connection line.
     *
     * @param parent        the parent UI component
     * @param previous      the previous UI component
     * @param middle        the middle
     * @param margin        the margin
     * @param wlConnection  the connection label
     * @param wbnConnection the "new connection" button
     * @param wbeConnection the "edit connection" button
     * @param databaseType  the database type
     * @return the Combo Box component for the given parameters
     */
    public CCombo addConnectionLine(Composite parent, Control previous, int middle, int margin,
            final Label wlConnection, final Button wbwConnection, final Button wbnConnection,
            final Button wbeConnection, final Class<? extends DatabaseInterface> databaseType) {
        final CCombo wConnection;
        final FormData fdlConnection, fdbConnection, fdeConnection, fdConnection, fdbwConnection;

        wConnection = new CCombo(parent, SWT.BORDER | SWT.READ_ONLY);
        props.setLook(wConnection);

        addDatabases(wConnection);

        wlConnection.setText(BaseMessages.getString(PKG, "BaseStepDialog.Connection.Label"));
        props.setLook(wlConnection);
        fdlConnection = new FormData();
        fdlConnection.left = new FormAttachment(0, 0);
        fdlConnection.right = new FormAttachment(middle, -margin);
        if (previous != null) {
            fdlConnection.top = new FormAttachment(previous, margin);
        } else {
            fdlConnection.top = new FormAttachment(0, 0);
        }
        wlConnection.setLayoutData(fdlConnection);

        //
        // Wizard button
        //
        wbwConnection.setText(BaseMessages.getString(PKG, "BaseStepDialog.WizardConnectionButton.Label"));
        wbwConnection.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                CreateDatabaseWizard cdw = new CreateDatabaseWizard();
                DatabaseMeta newDBInfo = cdw.createAndRunDatabaseWizard(shell, props, transMeta.getDatabases());
                if (newDBInfo != null) {
                    transMeta.addDatabase(newDBInfo);
                    reinitConnectionDropDown(wConnection, newDBInfo.getName());
                }
            }
        });
        fdbwConnection = new FormData();
        fdbwConnection.right = new FormAttachment(100, 0);
        if (previous != null) {
            fdbwConnection.top = new FormAttachment(previous, margin);
        } else {
            fdbwConnection.top = new FormAttachment(0, 0);
        }
        wbwConnection.setLayoutData(fdbwConnection);

        //
        // NEW button
        //
        wbnConnection.setText(BaseMessages.getString(PKG, "BaseStepDialog.NewConnectionButton.Label"));
        wbnConnection.addSelectionListener(new AddConnectionListener(wConnection));
        fdbConnection = new FormData();
        fdbConnection.right = new FormAttachment(wbwConnection, -margin);
        if (previous != null) {
            fdbConnection.top = new FormAttachment(previous, margin);
        } else {
            fdbConnection.top = new FormAttachment(0, 0);
        }
        wbnConnection.setLayoutData(fdbConnection);

        //
        // Edit button
        //
        wbeConnection.setText(BaseMessages.getString(PKG, "BaseStepDialog.EditConnectionButton.Label"));
        wbeConnection.addSelectionListener(new EditConnectionListener(wConnection));
        fdeConnection = new FormData();
        fdeConnection.right = new FormAttachment(wbnConnection, -margin);
        if (previous != null) {
            fdeConnection.top = new FormAttachment(previous, margin);
        } else {
            fdeConnection.top = new FormAttachment(0, 0);
        }
        wbeConnection.setLayoutData(fdeConnection);

        //
        // what's left of the line: combo box
        //
        fdConnection = new FormData();
        fdConnection.left = new FormAttachment(middle, 0);
        if (previous != null) {
            fdConnection.top = new FormAttachment(previous, margin);
        } else {
            fdConnection.top = new FormAttachment(0, 0);
        }
        fdConnection.right = new FormAttachment(wbeConnection, -margin);
        wConnection.setLayoutData(fdConnection);

        return wConnection;
    }

    @VisibleForTesting
    String showDbDialogUnlessCancelledOrValid(DatabaseMeta changing, DatabaseMeta origin) {
        changing.shareVariablesWith(transMeta);
        DatabaseDialog cid = getDatabaseDialog(shell);
        cid.setDatabaseMeta(changing);
        cid.setModalDialog(true);

        String name = null;
        boolean repeat = true;
        while (repeat) {
            name = cid.open();
            if (name == null) {
                // Cancel was pressed
                repeat = false;
            } else {
                name = name.trim();
                DatabaseMeta same = transMeta.findDatabase(name);
                if (same == null || same == origin) {
                    // OK was pressed and input is valid
                    repeat = false;
                } else {
                    showDbExistsDialog(changing);
                }
            }
        }
        return name;
    }

    @VisibleForTesting
    void showDbExistsDialog(DatabaseMeta changing) {
        DatabaseDialog.showDatabaseExistsDialog(shell, changing);
    }

    private void reinitConnectionDropDown(CCombo dropDown, String selected) {
        dropDown.removeAll();
        addDatabases(dropDown);
        selectDatabase(dropDown, selected);
    }

    /**
     * Gets the database dialog.
     *
     * @param shell the shell
     * @return the database dialog
     */
    protected DatabaseDialog getDatabaseDialog(Shell shell) {
        if (databaseDialog == null) {
            databaseDialog = new DatabaseDialog(shell);
        }
        return databaseDialog;
    }

    /**
     * Store screen size.
     */
    public void storeScreenSize() {
        props.setScreen(new WindowProperty(shell));
    }

    public String toString() {
        return this.getClass().getName();
    }

    /**
     * Gets the repository associated with this dialog.
     *
     * @return Returns the repository.
     */
    public Repository getRepository() {
        return repository;
    }

    /**
     * Sets the repository associated with this dialog.
     *
     * @param repository The repository to set.
     */
    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    /**
     * Sets the minimal shell height.
     *
     * @param shell    the shell
     * @param controls the controls to measure
     * @param margin   the margin between the components
     * @param extra    the extra padding
     */
    public static void setMinimalShellHeight(Shell shell, Control[] controls, int margin, int extra) {
        int height = 0;

        for (int i = 0; i < controls.length; i++) {
            Rectangle bounds = controls[i].getBounds();
            height += bounds.height + margin;
        }
        height += extra;
        shell.setSize(shell.getBounds().width, height);
    }

    /**
     * Sets the size of this dialog with respect to the given shell.
     *
     * @param shell the new size
     */
    public static void setSize(Shell shell) {
        setSize(shell, -1, -1, true);
    }

    public static void setSize(Shell shell, int prefWidth, int prefHeight) {
        PropsUI props = PropsUI.getInstance();

        WindowProperty winprop = props.getScreen(shell.getText());
        if (winprop != null) {
            winprop.setShell(shell, prefWidth, prefHeight);
        } else {
            shell.layout();

            winprop = new WindowProperty(shell.getText(), false, new Rectangle(0, 0, prefWidth, prefHeight));
            winprop.setShell(shell);

            // Now, as this is the first time it gets opened, try to put it in the middle of the screen...
            Rectangle shellBounds = shell.getBounds();
            Monitor monitor = shell.getDisplay().getPrimaryMonitor();
            if (shell.getParent() != null) {
                monitor = shell.getParent().getMonitor();
            }
            Rectangle monitorClientArea = monitor.getClientArea();

            int middleX = monitorClientArea.x + (monitorClientArea.width - shellBounds.width) / 2;
            int middleY = monitorClientArea.y + (monitorClientArea.height - shellBounds.height) / 2;

            shell.setLocation(middleX, middleY);
        }
    }

    /**
     * Sets the size of this dialog with respect to the given parameters.
     *
     * @param shell     the shell
     * @param minWidth  the minimum width
     * @param minHeight the minimum height
     * @param packIt    true to pack the dialog components, false otherwise
     */
    public static void setSize(Shell shell, int minWidth, int minHeight, boolean packIt) {
        PropsUI props = PropsUI.getInstance();

        WindowProperty winprop = props.getScreen(shell.getText());
        if (winprop != null) {
            winprop.setShell(shell, minWidth, minHeight);
        } else {
            if (packIt) {
                shell.pack();
            } else {
                shell.layout();
            }

            // OK, sometimes this produces dialogs that are waay too big.
            // Try to limit this a bit, m'kay?
            // Use the same algorithm by cheating :-)
            //
            winprop = new WindowProperty(shell);
            winprop.setShell(shell, minWidth, minHeight);

            // Now, as this is the first time it gets opened, try to put it in the middle of the screen...
            Rectangle shellBounds = shell.getBounds();
            Monitor monitor = shell.getDisplay().getPrimaryMonitor();
            if (shell.getParent() != null) {
                monitor = shell.getParent().getMonitor();
            }
            Rectangle monitorClientArea = monitor.getClientArea();

            int middleX = monitorClientArea.x + (monitorClientArea.width - shellBounds.width) / 2;
            int middleY = monitorClientArea.y + (monitorClientArea.height - shellBounds.height) / 2;

            shell.setLocation(middleX, middleY);
        }
    }

    /**
     * Sets the traverse order for the given controls.
     *
     * @param controls the new traverse order
     */
    public static final void setTraverseOrder(final Control[] controls) {
        for (int i = 0; i < controls.length; i++) {
            final int controlNr = i;
            if (i < controls.length - 1) {
                controls[i].addTraverseListener(new TraverseListener() {
                    public void keyTraversed(TraverseEvent te) {
                        te.doit = false;
                        // set focus on the next control.
                        // What is the next control?
                        int thisOne = controlNr + 1;
                        while (!controls[thisOne].isEnabled()) {
                            thisOne++;
                            if (thisOne >= controls.length) {
                                thisOne = 0;
                            }
                            if (thisOne == controlNr) {
                                return; // already tried all others, time to quit.
                            }
                        }
                        controls[thisOne].setFocus();
                    }
                });
            } else { // Link last item to first.

                controls[i].addTraverseListener(new TraverseListener() {
                    public void keyTraversed(TraverseEvent te) {
                        te.doit = false;
                        // set focus on the next control.
                        // set focus on the next control.
                        // What is the next control : 0
                        int thisOne = 0;
                        while (!controls[thisOne].isEnabled()) {
                            thisOne++;
                            if (thisOne >= controls.length) {
                                return; // already tried all others, time to quit.
                            }
                        }
                        controls[thisOne].setFocus();
                    }
                });
            }
        }
    }

    /**
     * Gets unused fields from previous steps and inserts them as rows into a table view.
     *
     * @param transMeta       the transformation metadata
     * @param stepMeta        the step metadata
     * @param tableView       the table view
     * @param keyColumn       the key column
     * @param nameColumn      the name column
     * @param dataTypeColumn  the data type column
     * @param lengthColumn    the length column
     * @param precisionColumn the precision column
     * @param listener        a listener for tables insert events
     */
    public static final void getFieldsFromPrevious(TransMeta transMeta, StepMeta stepMeta, TableView tableView,
            int keyColumn, int[] nameColumn, int[] dataTypeColumn, int lengthColumn, int precisionColumn,
            TableItemInsertListener listener) {
        try {
            RowMetaInterface row = transMeta.getPrevStepFields(stepMeta);
            if (row != null) {
                getFieldsFromPrevious(row, tableView, keyColumn, nameColumn, dataTypeColumn, lengthColumn,
                        precisionColumn, listener);
            }
        } catch (KettleException ke) {
            new ErrorDialog(tableView.getShell(),
                    BaseMessages.getString(PKG, "BaseStepDialog.FailedToGetFields.Title"),
                    BaseMessages.getString(PKG, "BaseStepDialog.FailedToGetFields.Message", stepMeta.getName()),
                    ke);
        }
    }

    /**
     * Gets unused fields from previous steps and inserts them as rows into a table view.
     *
     * @param row             the input fields
     * @param tableView       the table view to modify
     * @param keyColumn       the column in the table view to match with the names of the fields, checks for existance if
     *                        >0
     * @param nameColumn      the column numbers in which the name should end up in
     * @param dataTypeColumn  the target column numbers in which the data type should end up in
     * @param lengthColumn    the length column where the length should end up in (if >0)
     * @param precisionColumn the length column where the precision should end up in (if >0)
     * @param listener        A listener that you can use to do custom modifications to the inserted table item, based on
     *                        a value from the provided row
     */
    public static final void getFieldsFromPrevious(RowMetaInterface row, TableView tableView, int keyColumn,
            int[] nameColumn, int[] dataTypeColumn, int lengthColumn, int precisionColumn,
            TableItemInsertListener listener) {
        getFieldsFromPrevious(row, tableView, keyColumn, nameColumn, dataTypeColumn, lengthColumn, precisionColumn,
                true, listener);
    }

    /**
     * Gets unused fields from previous steps and inserts them as rows into a table view.
     *
     * @param row             the input fields
     * @param tableView       the table view to modify
     * @param keyColumn       the column in the table view to match with the names of the fields, checks for existance if
     *                        >0
     * @param nameColumn      the column numbers in which the name should end up in
     * @param dataTypeColumn  the target column numbers in which the data type should end up in
     * @param lengthColumn    the length column where the length should end up in (if >0)
     * @param precisionColumn the length column where the precision should end up in (if >0)
     * @param optimizeWidth
     * @param listener        A listener that you can use to do custom modifications to the inserted table item, based on
     *                        a value from the provided row
     */
    public static final void getFieldsFromPrevious(RowMetaInterface row, TableView tableView, int keyColumn,
            int[] nameColumn, int[] dataTypeColumn, int lengthColumn, int precisionColumn, boolean optimizeWidth,
            TableItemInsertListener listener) {
        getFieldsFromPrevious(row, tableView, keyColumn, nameColumn, dataTypeColumn, lengthColumn, precisionColumn,
                optimizeWidth, listener, BaseStepDialog::getFieldsChoiceDialog);
    }

    /**
     * Gets unused fields from previous steps and inserts them as rows into a table view.
     *
     * @param row             the input fields
     * @param tableView       the table view to modify
     * @param keyColumn       the column in the table view to match with the names of the fields, checks for existance if
     *                        >0
     * @param nameColumn      the column numbers in which the name should end up in
     * @param dataTypeColumn  the target column numbers in which the data type should end up in
     * @param lengthColumn    the length column where the length should end up in (if >0)
     * @param precisionColumn the length column where the precision should end up in (if >0)
     * @param optimizeWidth
     * @param listener        A listener that you can use to do custom modifications to the inserted table item, based on
     *                        a value from the provided row
     * @param getFieldsChoiceDialogProvider the GetFieldsChoice dialog provider
     */
    public static final void getFieldsFromPrevious(RowMetaInterface row, TableView tableView, int keyColumn,
            int[] nameColumn, int[] dataTypeColumn, int lengthColumn, int precisionColumn, boolean optimizeWidth,
            TableItemInsertListener listener, FieldsChoiceDialogProvider getFieldsChoiceDialogProvider) {
        if (row == null || row.size() == 0) {
            return; // nothing to do
        }

        Table table = tableView.table;

        // get a list of all the non-empty keys (names)
        //
        List<String> keys = new ArrayList<>();
        for (int i = 0; i < table.getItemCount(); i++) {
            TableItem tableItem = table.getItem(i);
            String key = tableItem.getText(keyColumn);
            if (!Utils.isEmpty(key) && keys.indexOf(key) < 0) {
                keys.add(key);
            }
        }

        int choice = 0;

        if (keys.size() > 0) {
            // Ask what we should do with the existing data in the step.
            //
            MessageDialog getFieldsChoiceDialog = getFieldsChoiceDialogProvider.provide(tableView.getShell(),
                    keys.size(), row.size());

            int idx = getFieldsChoiceDialog.open();
            choice = idx & 0xFF;
        }

        if (choice == 3 || choice == 255) {
            return; // Cancel clicked
        }

        if (choice == 2) {
            tableView.clearAll(false);
        }

        for (int i = 0; i < row.size(); i++) {
            ValueMetaInterface v = row.getValueMeta(i);

            boolean add = true;

            if (choice == 0) { // hang on, see if it's not yet in the table view

                if (keys.indexOf(v.getName()) >= 0) {
                    add = false;
                }
            }

            if (add) {
                TableItem tableItem = new TableItem(table, SWT.NONE);

                for (int c = 0; c < nameColumn.length; c++) {
                    tableItem.setText(nameColumn[c], Const.NVL(v.getName(), ""));
                }
                if (dataTypeColumn != null) {
                    for (int c = 0; c < dataTypeColumn.length; c++) {
                        tableItem.setText(dataTypeColumn[c], v.getTypeDesc());
                    }
                }
                if (lengthColumn > 0) {
                    if (v.getLength() >= 0) {
                        tableItem.setText(lengthColumn, Integer.toString(v.getLength()));
                    }
                }
                if (precisionColumn > 0) {
                    if (v.getPrecision() >= 0) {
                        tableItem.setText(precisionColumn, Integer.toString(v.getPrecision()));
                    }
                }

                if (listener != null) {
                    if (!listener.tableItemInserted(tableItem, v)) {
                        tableItem.dispose(); // remove it again
                    }
                }
            }
        }
        tableView.removeEmptyRows();
        tableView.setRowNums();
        if (optimizeWidth) {
            tableView.optWidth(true);
        }
    }

    static MessageDialog getFieldsChoiceDialog(Shell shell, int existingFields, int newFields) {
        MessageDialog messageDialog = new MessageDialog(shell,
                BaseMessages.getString(PKG, "BaseStepDialog.GetFieldsChoice.Title"), // "Warning!"
                null,
                BaseMessages.getString(
                        PKG, "BaseStepDialog.GetFieldsChoice.Message", "" + existingFields, "" + newFields),
                MessageDialog.WARNING,
                new String[] { BaseMessages.getString(PKG, "BaseStepDialog.AddNew"),
                        BaseMessages.getString(PKG, "BaseStepDialog.Add"),
                        BaseMessages.getString(PKG, "BaseStepDialog.ClearAndAdd"),
                        BaseMessages.getString(PKG, "BaseStepDialog.Cancel"), },
                0);
        MessageDialog.setDefaultImage(GUIResource.getInstance().getImageSpoon());
        return messageDialog;
    }

    /**
     * Gets fields from previous steps and populate a ComboVar.
     *
     * @param comboVar  the Combo Box (with Variables) to populate
     * @param transMeta the transformation metadata
     * @param stepMeta  the step metadata
     */
    public static final void getFieldsFromPrevious(ComboVar comboVar, TransMeta transMeta, StepMeta stepMeta) {
        String selectedField = null;
        int indexField = -1;
        try {
            RowMetaInterface r = transMeta.getPrevStepFields(stepMeta);
            selectedField = comboVar.getText();
            comboVar.removeAll();

            if (r != null && !r.isEmpty()) {
                r.getFieldNames();
                comboVar.setItems(r.getFieldNames());
                indexField = r.indexOfValue(selectedField);
            }
            // Select value if possible...
            if (indexField > -1) {
                comboVar.select(indexField);
            } else {
                if (selectedField != null) {
                    comboVar.setText(selectedField);
                }
            }
        } catch (KettleException ke) {
            new ErrorDialog(comboVar.getShell(),
                    BaseMessages.getString(PKG, "BaseStepDialog.FailedToGetFieldsPrevious.DialogTitle"),
                    BaseMessages.getString(PKG, "BaseStepDialog.FailedToGetFieldsPrevious.DialogMessage"), ke);
        }
    }

    /**
     * Create a new field mapping between source and target steps.
     *
     * @param shell        the shell of the parent window
     * @param sourceFields the source fields
     * @param targetFields the target fields
     * @param fieldMapping the list of source to target mappings to default to (can be empty but not null)
     * @throws KettleException in case something goes wrong during the field mapping
     */
    public static final void generateFieldMapping(Shell shell, RowMetaInterface sourceFields,
            RowMetaInterface targetFields, List<SourceToTargetMapping> fieldMapping) throws KettleException {
        // Build the mapping: let the user decide!!
        String[] source = sourceFields.getFieldNames();
        for (int i = 0; i < source.length; i++) {
            ValueMetaInterface v = sourceFields.getValueMeta(i);
            source[i] += EnterMappingDialog.STRING_ORIGIN_SEPARATOR + v.getOrigin() + ")";
        }
        String[] target = targetFields.getFieldNames();

        EnterMappingDialog dialog = new EnterMappingDialog(shell, source, target, fieldMapping);
        List<SourceToTargetMapping> newMapping = dialog.open();
        if (newMapping != null) {
            fieldMapping.clear();
            fieldMapping.addAll(newMapping);
        }
    }

    /**
     * Checks if the log level is basic.
     *
     * @return true, if the log level is basic, false otherwise
     */
    public boolean isBasic() {
        return log.isBasic();
    }

    /**
     * Checks if the log level is detailed.
     *
     * @return true, if the log level is detailed, false otherwise
     */
    public boolean isDetailed() {
        return log.isDetailed();
    }

    /**
     * Checks if the log level is debug.
     *
     * @return true, if the log level is debug, false otherwise
     */
    public boolean isDebug() {
        return log.isDebug();
    }

    /**
     * Checks if the log level is row level.
     *
     * @return true, if the log level is row level, false otherwise
     */
    public boolean isRowLevel() {
        return log.isRowLevel();
    }

    /**
     * Log the message at a minimal logging level.
     *
     * @param message the message to log
     */
    public void logMinimal(String message) {
        log.logMinimal(message);
    }

    /**
     * Log the message with arguments at a minimal logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logMinimal(String message, Object... arguments) {
        log.logMinimal(message, arguments);
    }

    /**
     * Log the message at a basic logging level.
     *
     * @param message the message
     */
    public void logBasic(String message) {
        log.logBasic(message);
    }

    /**
     * Log the message with arguments at a basic logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logBasic(String message, Object... arguments) {
        log.logBasic(message, arguments);
    }

    /**
     * Log the message at a detailed logging level.
     *
     * @param message the message
     */
    public void logDetailed(String message) {
        log.logDetailed(message);
    }

    /**
     * Log the message with arguments at a detailed logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logDetailed(String message, Object... arguments) {
        log.logDetailed(message, arguments);
    }

    /**
     * Log the message at a debug logging level.
     *
     * @param message the message
     */
    public void logDebug(String message) {
        log.logDebug(message);
    }

    /**
     * Log the message with arguments at a debug logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logDebug(String message, Object... arguments) {
        log.logDebug(message, arguments);
    }

    /**
     * Log the message at a rowlevel logging level.
     *
     * @param message the message
     */
    public void logRowlevel(String message) {
        log.logRowlevel(message);
    }

    /**
     * Log the message with arguments at a rowlevel logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logRowlevel(String message, Object... arguments) {
        log.logRowlevel(message, arguments);
    }

    /**
     * Log the message at a error logging level.
     *
     * @param message the message
     */
    public void logError(String message) {
        log.logError(message);
    }

    /**
     * Log the message with the associated Throwable object at a error logging level.
     *
     * @param message the message
     * @param e       the e
     */
    public void logError(String message, Throwable e) {
        log.logError(message, e);
    }

    /**
     * Log the message with arguments at a error logging level.
     *
     * @param message   the message
     * @param arguments the arguments
     */
    public void logError(String message, Object... arguments) {
        log.logError(message, arguments);
    }

    protected Button createHelpButton(final Shell shell, final StepMeta stepMeta, final PluginInterface plugin) {
        return HelpUtils.createHelpButton(shell, HelpUtils.getHelpDialogTitle(plugin), plugin);
    }

    private void setShellImage(Shell shell) {
        if (stepMeta != null) {
            PluginInterface plugin = PluginRegistry.getInstance().getPlugin(StepPluginType.class,
                    stepMeta.getStepMetaInterface());
            createHelpButton(shell, stepMeta, plugin);
            String id = plugin.getIds()[0];
            if (id != null) {
                shell.setImage(GUIResource.getInstance().getImagesSteps().get(id)
                        .getAsBitmapForSize(shell.getDisplay(), ConstUI.ICON_SIZE, ConstUI.ICON_SIZE));
            }
        }
    }

    public IMetaStore getMetaStore() {
        return metaStore;
    }

    public void setMetaStore(IMetaStore metaStore) {
        this.metaStore = metaStore;
    }

    protected String getPathOf(RepositoryElementMetaInterface object) {
        return DialogUtils.getPathOf(object);
    }

    @VisibleForTesting
    class AddConnectionListener extends SelectionAdapter {

        private final CCombo wConnection;

        public AddConnectionListener(CCombo wConnection) {
            this.wConnection = wConnection;
        }

        @Override
        public void widgetSelected(SelectionEvent e) {
            DatabaseMeta databaseMeta = new DatabaseMeta();
            String connectionName = showDbDialogUnlessCancelledOrValid(databaseMeta, null);
            if (connectionName != null) {
                transMeta.addDatabase(databaseMeta);
                reinitConnectionDropDown(wConnection, databaseMeta.getName());
            }
        }
    }

    public interface FieldsChoiceDialogProvider {
        MessageDialog provide(Shell shell, int existingFields, int newFields);
    }

    @VisibleForTesting
    class EditConnectionListener extends SelectionAdapter {

        private final CCombo wConnection;

        public EditConnectionListener(CCombo wConnection) {
            this.wConnection = wConnection;
        }

        public void widgetSelected(SelectionEvent e) {
            DatabaseMeta databaseMeta = transMeta.findDatabase(wConnection.getText());
            if (databaseMeta != null) {
                // cloning to avoid spoiling data on cancel or incorrect input
                DatabaseMeta clone = (DatabaseMeta) databaseMeta.clone();
                // setting old Id, so a repository (if it used) could find and replace the existing connection
                clone.setObjectId(databaseMeta.getObjectId());
                String connectionName = showDbDialogUnlessCancelledOrValid(clone, databaseMeta);
                if (connectionName != null) {
                    // need to replace the old connection with a new one
                    if (databaseMeta.isShared()) {
                        if (!replaceSharedConnection(databaseMeta, clone)) {
                            return;
                        }
                    }
                    transMeta.removeDatabase(transMeta.indexOfDatabase(databaseMeta));
                    transMeta.addDatabase(clone);
                    reinitConnectionDropDown(wConnection, connectionName);
                }
            }
        }

        boolean replaceSharedConnection(DatabaseMeta dbConnection, DatabaseMeta newDbConnection) {
            try {
                SharedObjects sharedObjects = transMeta.getSharedObjects();
                sharedObjects.removeObject(dbConnection);
                sharedObjects.storeObject(newDbConnection);
                sharedObjects.saveToFile();
                return true;
            } catch (Exception e) {
                showErrorDialog(e);
                return false;
            }
        }

        void showErrorDialog(Exception e) {
            new ErrorDialog(wConnection.getShell(),
                    BaseMessages.getString(PKG, "BaseStep.Exception.UnexpectedErrorEditingConnection.DialogTitle"),
                    BaseMessages.getString(PKG,
                            "BaseStep.Exception.UnexpectedErrorEditingConnection.DialogMessage"),
                    e);
        }
    }
}