org.signserver.admin.gui.AddWorkerDialog.java Source code

Java tutorial

Introduction

Here is the source code for org.signserver.admin.gui.AddWorkerDialog.java

Source

/*************************************************************************
 *                                                                       *
 *  SignServer: The OpenSource Automated Signing Server                  *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.signserver.admin.gui;

import java.awt.CardLayout;
import java.awt.Component;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Properties;
import javax.ejb.EJBException;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.JTextComponent;
import javax.xml.ws.soap.SOAPFaultException;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.jdesktop.application.Action;
import org.jdesktop.application.Task;
import org.signserver.admin.gui.adminws.gen.AdminNotAuthorizedException_Exception;
import org.signserver.common.util.PropertiesApplier;
import org.signserver.common.util.PropertiesConstants;
import org.signserver.common.util.PropertiesParser;

/**
 * Dialog for adding worker(s) from a properties file, or by editing
 * properties manually.
 * 
 * @author Marcus Lundblad
 * @version $Id: AddWorkerDialog.java 4773 2014-05-27 10:02:42Z netmackan $
 */
public class AddWorkerDialog extends javax.swing.JDialog {

    /** Logger for this class. */
    private static final Logger LOG = Logger.getLogger(AddWorkerDialog.class);

    /**
     * Enum holding state in the config dialog.
     * 
     */
    private enum Stage {

        /**
         * The initial state, choosing a file or entering preset configuration.
         */
        INITIAL_CONFIG,
        /**
         * The final state, with the possibility to hand-edit properties.
         */
        EDIT_PROPERTIES
    }

    /**
     * Enum holding the add worker mode.
     */
    private enum Mode {

        /**
         * Load worker properties from a property file.
         */
        LOAD_FROM_FILE,
        /**
         * Edit worker properties in the UI.
         */
        EDIT_MANUALLY
    }

    private Stage stage;
    private Mode mode;
    // raw data of the config
    private String config;
    // keep track of raw configuration editing
    private boolean configurationEdited = false;
    // keep track of newly selected file
    private boolean fileSelected = false;

    private List<Integer> modifiedWorkers;

    private WorkerPropertyEditor workerPropertyEditor = new WorkerPropertyEditor();

    /** Creates new form AddWorkerDialog */
    public AddWorkerDialog(java.awt.Frame parent, boolean modal) {
        super(parent, modal);
        initComponents();

        stage = Stage.INITIAL_CONFIG;
        mode = Mode.LOAD_FROM_FILE;
        loadFromFileRadioButton.setSelected(true);
        updateControls();

        // initially set the Next button to be greyed-out, so that it can be
        // enabled based on the state
        nextApplyButton.setEnabled(false);

        invalidWorkerIdStatusLabel.setVisible(false);

        propertiesTable.setDefaultRenderer(String.class, new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value, boolean selected,
                    boolean focused, int row, int column) {
                setEnabled(table == null || table.isEnabled());
                super.getTableCellRendererComponent(table, value, selected, focused, row, column);

                return this;
            }
        });

        propertiesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {

            @Override
            public void valueChanged(final ListSelectionEvent e) {
                if (!e.getValueIsAdjusting()) {
                    final boolean enable = propertiesTable.getSelectedRowCount() == 1;
                    editPropertyButton.setEnabled(enable);
                    removePropertyButton.setEnabled(enable);
                }
            }
        });

        // add a document listner to update the UI when the content of the ID
        // combobox changes
        final JTextComponent component = (JTextComponent) workerIdComboBox.getEditor().getEditorComponent();
        component.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent ev) {
                updateControls();
            }

            @Override
            public void removeUpdate(DocumentEvent ev) {
                updateControls();
            }

            @Override
            public void changedUpdate(DocumentEvent ev) {
                updateControls();
            }
        });

        // add a document listner to update the UI on updates of the configuration text
        configurationTextArea.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent ev) {
                configurationEdited = true;
                updateControls();
            }

            @Override
            public void removeUpdate(DocumentEvent ev) {
                configurationEdited = true;
                updateControls();
            }

            @Override
            public void changedUpdate(DocumentEvent ev) {
                configurationEdited = true;
                updateControls();
            }
        });
    }

    /**
     * Update the UI according to the stage, changing button visibility 
     */
    private void updateControls() {
        // the reload button is only visible in the edit properties stage
        resetButton.setVisible(stage == Stage.EDIT_PROPERTIES);
        // enable the reload button when there is changes done in the
        // free text configuration editor
        resetButton.setEnabled(configurationEdited);

        // only enable the back button in the last step...
        backButton.setEnabled(stage == Stage.EDIT_PROPERTIES);
        // similarily, set the appropriate text for the "Next"/"Apply" button
        nextApplyButton.setText(stage == Stage.INITIAL_CONFIG ? "Next" : "Apply");

        // update controls depending on mode (load from file or edit manually)

        // file controls
        filePathTextField.setEnabled(mode == Mode.LOAD_FROM_FILE);
        filePathBrowseButton.setEnabled(mode == Mode.LOAD_FROM_FILE);

        // edit controls
        workerIdLabel.setEnabled(mode == Mode.EDIT_MANUALLY);
        workerIdComboBox.setEnabled(mode == Mode.EDIT_MANUALLY);
        workerNameLabel.setEnabled(mode == Mode.EDIT_MANUALLY);
        workerNameField.setEnabled(mode == Mode.EDIT_MANUALLY);
        workerImplementationLabel.setEnabled(mode == Mode.EDIT_MANUALLY);
        workerImplementationField.setEnabled(mode == Mode.EDIT_MANUALLY);
        tokenImplementationLabel.setEnabled(mode == Mode.EDIT_MANUALLY);
        tokenImplementationField.setEnabled(mode == Mode.EDIT_MANUALLY);
        propertiesLabel.setEnabled(mode == Mode.EDIT_MANUALLY);
        propertiesScrollPanel.setEnabled(mode == Mode.EDIT_MANUALLY);
        propertiesTable.setEnabled(mode == Mode.EDIT_MANUALLY);

        final int selectedRows = propertiesTable.getSelectedRowCount();

        addPropertyButton.setEnabled(mode == Mode.EDIT_MANUALLY);
        removePropertyButton.setEnabled(mode == Mode.EDIT_MANUALLY && selectedRows == 1);
        editPropertyButton.setEnabled(mode == Mode.EDIT_MANUALLY && selectedRows == 1);

        // update state of Next/Apply button
        switch (mode) {
        case LOAD_FROM_FILE:
            final String filePath = filePathTextField.getText();
            nextApplyButton.setEnabled(filePath != null && !filePath.isEmpty());
            invalidWorkerIdStatusLabel.setVisible(false);
            break;
        case EDIT_MANUALLY:
            final String workerId = ((JTextField) workerIdComboBox.getEditor().getEditorComponent()).getText();
            final String workerName = workerNameField.getText();
            final String classPath = workerImplementationField.getText();
            final boolean workerIdValid = isWorkerIdValid(workerId);

            // enable next button if all required fields have been set
            nextApplyButton.setEnabled(
                    !workerId.isEmpty() && workerIdValid && !workerName.isEmpty() && !classPath.isEmpty());
            invalidWorkerIdStatusLabel.setVisible(!workerIdValid);

            break;
        default:
            // should not happen
            break;
        }
    }

    /**
     * Determine if a string qualifies as a worker ID prefix, either a positive
     * integer or a string of the form GENIDx, where x is a positive integer
     * for generated worker IDs.
     * 
     * @param workerId
     * @return True if the given string represents a valid worker ID or generated ID
     */
    private boolean isWorkerIdValid(final String workerId) {
        try {
            final int id = Integer.parseInt(workerId);

            return id > 0;
        } catch (NumberFormatException e) {
            if (workerId.length() > PropertiesConstants.GENID.length() && PropertiesConstants.GENID
                    .equals(workerId.substring(0, PropertiesConstants.GENID.length()))) {
                try {
                    final int index = Integer.parseInt(workerId.substring(PropertiesConstants.GENID.length()));

                    return index >= 1;
                } catch (NumberFormatException ex) {
                    return false;
                }
            } else {
                return false;
            }
        }
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        modeSelectButtonGroup = new javax.swing.ButtonGroup();
        nextApplyButton = new javax.swing.JButton();
        backButton = new javax.swing.JButton();
        resetButton = new javax.swing.JButton();
        cancelButton = new javax.swing.JButton();
        wizardPanel = new javax.swing.JPanel();
        initialSetupPanel = new javax.swing.JPanel();
        removePropertyButton = new javax.swing.JButton();
        propertiesScrollPanel = new javax.swing.JScrollPane();
        propertiesTable = new javax.swing.JTable();
        addPropertyButton = new javax.swing.JButton();
        editPropertyButton = new javax.swing.JButton();
        filePathTextField = new javax.swing.JTextField();
        propertiesLabel = new javax.swing.JLabel();
        filePathBrowseButton = new javax.swing.JButton();
        loadFromFileRadioButton = new javax.swing.JRadioButton();
        editWorkerPropertiesRadioButton = new javax.swing.JRadioButton();
        workerIdLabel = new javax.swing.JLabel();
        workerIdComboBox = new javax.swing.JComboBox();
        workerNameLabel = new javax.swing.JLabel();
        workerNameField = new javax.swing.JTextField();
        workerImplementationLabel = new javax.swing.JLabel();
        workerImplementationField = new javax.swing.JTextField();
        tokenImplementationLabel = new javax.swing.JLabel();
        tokenImplementationField = new javax.swing.JTextField();
        invalidWorkerIdStatusLabel = new javax.swing.JLabel();
        configurationPanel = new javax.swing.JPanel();
        configurationLabel = new javax.swing.JLabel();
        configurationScrollPane = new javax.swing.JScrollPane();
        configurationTextArea = new javax.swing.JTextArea();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        org.jdesktop.application.ResourceMap resourceMap = org.jdesktop.application.Application
                .getInstance(org.signserver.admin.gui.SignServerAdminGUIApplication.class).getContext()
                .getResourceMap(AddWorkerDialog.class);
        setTitle(resourceMap.getString("Form.title")); // NOI18N
        setName("Form"); // NOI18N

        javax.swing.ActionMap actionMap = org.jdesktop.application.Application
                .getInstance(org.signserver.admin.gui.SignServerAdminGUIApplication.class).getContext()
                .getActionMap(AddWorkerDialog.class, this);
        nextApplyButton.setAction(actionMap.get("nextAction")); // NOI18N
        nextApplyButton.setText(resourceMap.getString("nextApplyButton.text")); // NOI18N
        nextApplyButton.setName("nextApplyButton"); // NOI18N

        backButton.setText(resourceMap.getString("backButton.text")); // NOI18N
        backButton.setName("backButton"); // NOI18N
        backButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                backButtonActionPerformed(evt);
            }
        });

        resetButton.setAction(actionMap.get("reloadAction")); // NOI18N
        resetButton.setText(resourceMap.getString("resetButton.text")); // NOI18N
        resetButton.setName("resetButton"); // NOI18N

        cancelButton.setText(resourceMap.getString("cancelButton.text")); // NOI18N
        cancelButton.setName("cancelButton"); // NOI18N
        cancelButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                cancelButtonActionPerformed(evt);
            }
        });

        wizardPanel.setName("wizardPanel"); // NOI18N
        wizardPanel.setLayout(new java.awt.CardLayout());

        initialSetupPanel.setName("initialSetupPanel"); // NOI18N

        removePropertyButton.setText(resourceMap.getString("removePropertyButton.text")); // NOI18N
        removePropertyButton.setName("removePropertyButton"); // NOI18N
        removePropertyButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                removePropertyButtonActionPerformed(evt);
            }
        });

        propertiesScrollPanel.setName("propertiesScrollPanel"); // NOI18N

        propertiesTable.setModel(new javax.swing.table.DefaultTableModel(new Object[][] {

        }, new String[] { "Property", "Value" }));
        propertiesTable.setName("propertiesTable"); // NOI18N
        propertiesScrollPanel.setViewportView(propertiesTable);

        addPropertyButton.setText(resourceMap.getString("addPropertyButton.text")); // NOI18N
        addPropertyButton.setName("addPropertyButton"); // NOI18N
        addPropertyButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                addPropertyButtonActionPerformed(evt);
            }
        });

        editPropertyButton.setText(resourceMap.getString("editPropertyButton.text")); // NOI18N
        editPropertyButton.setName("editPropertyButton"); // NOI18N
        editPropertyButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                editPropertyButtonActionPerformed(evt);
            }
        });

        filePathTextField.setText(resourceMap.getString("filePathTextField.text")); // NOI18N
        filePathTextField.setName("filePathTextField"); // NOI18N
        filePathTextField.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                filePathTextFieldActionPerformed(evt);
            }
        });
        filePathTextField.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyTyped(java.awt.event.KeyEvent evt) {
                filePathTextFieldKeyTyped(evt);
            }
        });

        propertiesLabel.setFont(resourceMap.getFont("propertiesLabel.font")); // NOI18N
        propertiesLabel.setText(resourceMap.getString("propertiesLabel.text")); // NOI18N
        propertiesLabel.setName("propertiesLabel"); // NOI18N

        filePathBrowseButton.setText(resourceMap.getString("filePathBrowseButton.text")); // NOI18N
        filePathBrowseButton.setName("filePathBrowseButton"); // NOI18N
        filePathBrowseButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                filePathBrowseButtonActionPerformed(evt);
            }
        });

        modeSelectButtonGroup.add(loadFromFileRadioButton);
        loadFromFileRadioButton.setFont(resourceMap.getFont("loadFromFileRadioButton.font")); // NOI18N
        loadFromFileRadioButton.setText(resourceMap.getString("loadFromFileRadioButton.text")); // NOI18N
        loadFromFileRadioButton.setName("loadFromFileRadioButton"); // NOI18N
        loadFromFileRadioButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                loadFromFileRadioButtonActionPerformed(evt);
            }
        });

        modeSelectButtonGroup.add(editWorkerPropertiesRadioButton);
        editWorkerPropertiesRadioButton.setFont(resourceMap.getFont("editWorkerPropertiesRadioButton.font")); // NOI18N
        editWorkerPropertiesRadioButton.setText(resourceMap.getString("editWorkerPropertiesRadioButton.text")); // NOI18N
        editWorkerPropertiesRadioButton.setName("editWorkerPropertiesRadioButton"); // NOI18N
        editWorkerPropertiesRadioButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                editWorkerPropertiesRadioButtonActionPerformed(evt);
            }
        });

        workerIdLabel.setText(resourceMap.getString("workerIdLabel.text")); // NOI18N
        workerIdLabel.setName("workerIdLabel"); // NOI18N

        workerIdComboBox.setEditable(true);
        workerIdComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "GENID1" }));
        workerIdComboBox.setName("workerIdComboBox"); // NOI18N

        workerNameLabel.setText(resourceMap.getString("workerNameLabel.text")); // NOI18N
        workerNameLabel.setName("workerNameLabel"); // NOI18N

        workerNameField.setName("workerNameField"); // NOI18N
        workerNameField.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyTyped(java.awt.event.KeyEvent evt) {
                workerNameFieldKeyTyped(evt);
            }
        });

        workerImplementationLabel.setText(resourceMap.getString("workerImplementationLabel.text")); // NOI18N
        workerImplementationLabel.setName("workerImplementationLabel"); // NOI18N

        workerImplementationField.setName("workerImplementationField"); // NOI18N
        workerImplementationField.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyTyped(java.awt.event.KeyEvent evt) {
                workerImplementationFieldKeyTyped(evt);
            }
        });

        tokenImplementationLabel.setText(resourceMap.getString("tokenImplementationLabel.text")); // NOI18N
        tokenImplementationLabel.setName("tokenImplementationLabel"); // NOI18N

        tokenImplementationField.setName("tokenImplementationField"); // NOI18N

        invalidWorkerIdStatusLabel.setForeground(resourceMap.getColor("invalidWorkerIdStatusLabel.foreground")); // NOI18N
        invalidWorkerIdStatusLabel.setText(resourceMap.getString("invalidWorkerIdStatusLabel.text")); // NOI18N
        invalidWorkerIdStatusLabel.setName("invalidWorkerIdStatusLabel"); // NOI18N

        javax.swing.GroupLayout initialSetupPanelLayout = new javax.swing.GroupLayout(initialSetupPanel);
        initialSetupPanel.setLayout(initialSetupPanelLayout);
        initialSetupPanelLayout.setHorizontalGroup(initialSetupPanelLayout
                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(initialSetupPanelLayout.createSequentialGroup().addContainerGap()
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addGroup(initialSetupPanelLayout.createSequentialGroup()
                                        .addComponent(invalidWorkerIdStatusLabel,
                                                javax.swing.GroupLayout.DEFAULT_SIZE, 906, Short.MAX_VALUE)
                                        .addContainerGap())
                                .addGroup(javax.swing.GroupLayout.Alignment.TRAILING,
                                        initialSetupPanelLayout.createSequentialGroup()
                                                .addComponent(filePathTextField,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, 874, Short.MAX_VALUE)
                                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                                .addComponent(filePathBrowseButton).addContainerGap())
                                .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, initialSetupPanelLayout
                                        .createSequentialGroup()
                                        .addComponent(propertiesScrollPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
                                                799, Short.MAX_VALUE)
                                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                        .addGroup(initialSetupPanelLayout
                                                .createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING,
                                                        false)
                                                .addComponent(editPropertyButton,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                                .addComponent(addPropertyButton,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                                .addComponent(removePropertyButton))
                                        .addContainerGap())
                                .addGroup(initialSetupPanelLayout.createSequentialGroup()
                                        .addComponent(loadFromFileRadioButton,
                                                javax.swing.GroupLayout.PREFERRED_SIZE, 133,
                                                javax.swing.GroupLayout.PREFERRED_SIZE)
                                        .addContainerGap(785, Short.MAX_VALUE))
                                .addGroup(initialSetupPanelLayout.createSequentialGroup()
                                        .addComponent(editWorkerPropertiesRadioButton).addGap(195, 195, 195))
                                .addComponent(propertiesLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 745,
                                        javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addGroup(initialSetupPanelLayout.createSequentialGroup()
                                        .addGroup(initialSetupPanelLayout
                                                .createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING,
                                                        false)
                                                .addComponent(tokenImplementationLabel,
                                                        javax.swing.GroupLayout.Alignment.LEADING,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                                .addComponent(workerImplementationLabel,
                                                        javax.swing.GroupLayout.Alignment.LEADING,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                                .addComponent(workerNameLabel,
                                                        javax.swing.GroupLayout.Alignment.LEADING,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                                .addComponent(workerIdLabel,
                                                        javax.swing.GroupLayout.Alignment.LEADING,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, 253, Short.MAX_VALUE))
                                        .addGap(18, 18, 18)
                                        .addGroup(initialSetupPanelLayout
                                                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                                .addComponent(workerIdComboBox,
                                                        javax.swing.GroupLayout.PREFERRED_SIZE, 131,
                                                        javax.swing.GroupLayout.PREFERRED_SIZE)
                                                .addComponent(workerNameField, javax.swing.GroupLayout.DEFAULT_SIZE,
                                                        635, Short.MAX_VALUE)
                                                .addComponent(workerImplementationField,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, 635, Short.MAX_VALUE)
                                                .addComponent(tokenImplementationField,
                                                        javax.swing.GroupLayout.DEFAULT_SIZE, 635, Short.MAX_VALUE))
                                        .addContainerGap()))));
        initialSetupPanelLayout.setVerticalGroup(initialSetupPanelLayout
                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(initialSetupPanelLayout.createSequentialGroup().addContainerGap()
                        .addComponent(loadFromFileRadioButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(filePathTextField, javax.swing.GroupLayout.PREFERRED_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addComponent(filePathBrowseButton))
                        .addGap(18, 18, 18).addComponent(editWorkerPropertiesRadioButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(workerIdLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 31,
                                        javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addComponent(workerIdComboBox, javax.swing.GroupLayout.PREFERRED_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(workerNameLabel)
                                .addComponent(workerNameField, javax.swing.GroupLayout.PREFERRED_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(workerImplementationLabel).addComponent(workerImplementationField,
                                        javax.swing.GroupLayout.PREFERRED_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(tokenImplementationLabel).addComponent(tokenImplementationField,
                                        javax.swing.GroupLayout.PREFERRED_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGap(18, 18, 18).addComponent(propertiesLabel)
                        .addGroup(initialSetupPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addGroup(initialSetupPanelLayout.createSequentialGroup().addGap(29, 29, 29)
                                        .addComponent(addPropertyButton).addGap(10, 10, 10)
                                        .addComponent(editPropertyButton)
                                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                        .addComponent(removePropertyButton))
                                .addGroup(initialSetupPanelLayout.createSequentialGroup()
                                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                        .addComponent(propertiesScrollPanel, javax.swing.GroupLayout.PREFERRED_SIZE,
                                                189, javax.swing.GroupLayout.PREFERRED_SIZE)))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(invalidWorkerIdStatusLabel).addContainerGap()));

        wizardPanel.add(initialSetupPanel, "initial");

        configurationPanel.setName("configurationPanel"); // NOI18N

        configurationLabel.setFont(resourceMap.getFont("configurationLabel.font")); // NOI18N
        configurationLabel.setText(resourceMap.getString("configurationLabel.text")); // NOI18N
        configurationLabel.setName("configurationLabel"); // NOI18N

        configurationScrollPane.setName("configurationScrollPane"); // NOI18N

        configurationTextArea.setColumns(20);
        configurationTextArea.setName("configurationTextArea"); // NOI18N
        configurationScrollPane.setViewportView(configurationTextArea);

        javax.swing.GroupLayout configurationPanelLayout = new javax.swing.GroupLayout(configurationPanel);
        configurationPanel.setLayout(configurationPanelLayout);
        configurationPanelLayout.setHorizontalGroup(
                configurationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                        .addGroup(configurationPanelLayout.createSequentialGroup().addGroup(configurationPanelLayout
                                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addComponent(configurationLabel)
                                .addGroup(configurationPanelLayout.createSequentialGroup().addGap(12, 12, 12)
                                        .addComponent(configurationScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE,
                                                906, Short.MAX_VALUE)))
                                .addContainerGap()));
        configurationPanelLayout.setVerticalGroup(
                configurationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                        .addGroup(configurationPanelLayout.createSequentialGroup().addComponent(configurationLabel)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(configurationScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 502,
                                        Short.MAX_VALUE)
                                .addContainerGap()));

        wizardPanel.add(configurationPanel, "editing");

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(layout.createSequentialGroup().addContainerGap().addComponent(resetButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 538, Short.MAX_VALUE)
                        .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, 86,
                                javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(backButton, javax.swing.GroupLayout.PREFERRED_SIZE, 74,
                                javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(nextApplyButton, javax.swing.GroupLayout.PREFERRED_SIZE, 110,
                                javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addContainerGap())
                .addGroup(
                        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addGroup(layout.createSequentialGroup().addContainerGap()
                                        .addComponent(wizardPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
                                                javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                        .addContainerGap())));

        layout.linkSize(javax.swing.SwingConstants.HORIZONTAL,
                new java.awt.Component[] { backButton, cancelButton, nextApplyButton });

        layout.setVerticalGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(javax.swing.GroupLayout.Alignment.TRAILING,
                        layout.createSequentialGroup().addContainerGap(592, Short.MAX_VALUE)
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                        .addComponent(resetButton).addComponent(nextApplyButton)
                                        .addComponent(backButton).addComponent(cancelButton))
                                .addContainerGap())
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                        .addGroup(layout.createSequentialGroup().addGap(37, 37, 37)
                                .addComponent(wizardPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
                                        javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addGap(59, 59, 59))));

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void filePathBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_filePathBrowseButtonActionPerformed
        final JFileChooser chooser = new JFileChooser();
        final File baseDir = SignServerAdminGUIApplication.getBaseDir();
        final String basedirPath = baseDir.getAbsolutePath();
        final File sampleDir = new File(basedirPath + File.separator + "doc" + File.separator + "sample-configs");

        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setCurrentDirectory(sampleDir.isDirectory() ? sampleDir : baseDir);
        chooser.setFileFilter(new FileNameExtensionFilter("Properties files", "properties"));

        final int res = chooser.showOpenDialog(this);

        if (res == JFileChooser.APPROVE_OPTION) {
            final File file = chooser.getSelectedFile();
            filePathTextField.setText(file.getAbsolutePath());
            fileSelected = true;
        }

        updateControls();
    }//GEN-LAST:event_filePathBrowseButtonActionPerformed

    private void filePathTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_filePathTextFieldActionPerformed
        // update UI controls
        updateControls();
    }//GEN-LAST:event_filePathTextFieldActionPerformed

    private void filePathTextFieldKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_filePathTextFieldKeyTyped
        // update UI controls
        updateControls();
        fileSelected = true;
    }//GEN-LAST:event_filePathTextFieldKeyTyped

    private void backButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_backButtonActionPerformed
        if (configurationEdited) {
            final int confirm = JOptionPane.showConfirmDialog(this,
                    "Configuration has been edited, going back to the initial setup will discard changes.\n Proceed?",
                    "Changes have been made", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);

            if (confirm == JOptionPane.YES_OPTION) {
                goBackToInitialConfig();
            }
        } else {
            // no changes made
            goBackToInitialConfig();
        }
    }//GEN-LAST:event_backButtonActionPerformed

    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed
        dispose();
    }//GEN-LAST:event_cancelButtonActionPerformed

    private void loadFromFileRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loadFromFileRadioButtonActionPerformed
        setMode(Mode.LOAD_FROM_FILE);
    }//GEN-LAST:event_loadFromFileRadioButtonActionPerformed

    private void editWorkerPropertiesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editWorkerPropertiesRadioButtonActionPerformed
        setMode(Mode.EDIT_MANUALLY);
    }//GEN-LAST:event_editWorkerPropertiesRadioButtonActionPerformed

    private void addOrEditProperty(final String key, final String value) {
        if (PropertiesConstants.NAME.equals(key)) {
            JOptionPane.showMessageDialog(this, "Use the Name text field to edit the worker name",
                    "Set worker name", JOptionPane.ERROR_MESSAGE);
        } else {
            final DefaultTableModel model = (DefaultTableModel) propertiesTable.getModel();
            boolean existing = false;

            for (int i = 0; i < model.getRowCount(); i++) {
                final String foundKey = (String) model.getValueAt(i, 0);

                if (key.equals(foundKey)) {
                    // update existing row
                    model.setValueAt(key, i, 0);
                    model.setValueAt(value, i, 1);
                    existing = true;
                    break;
                }
            }

            if (!existing) {
                model.addRow(new Object[] { key, value });
            }
        }
    }

    private void removeProperty(final String key) {
        final DefaultTableModel model = (DefaultTableModel) propertiesTable.getModel();

        for (int i = 0; i < model.getRowCount(); i++) {
            if (key.equals(model.getValueAt(i, 0))) {
                model.removeRow(i);
                break;
            }
        }
    }

    private void addPropertyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addPropertyButtonActionPerformed
        workerPropertyEditor.setKey("");
        workerPropertyEditor.setValue("");

        final int res = workerPropertyEditor.showDialog(this);

        if (res == JOptionPane.OK_OPTION) {
            final String key = workerPropertyEditor.getKey().toUpperCase();
            final String value = workerPropertyEditor.getValue();

            addOrEditProperty(key, value);
        }
    }//GEN-LAST:event_addPropertyButtonActionPerformed

    private void editPropertyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editPropertyButtonActionPerformed
        final int row = propertiesTable.getSelectedRow();

        if (row != -1) {
            final String oldKey = (String) propertiesTable.getValueAt(row, 0);
            final String oldValue = (String) propertiesTable.getValueAt(row, 1);

            workerPropertyEditor.setKey(oldKey);
            workerPropertyEditor.setValue(oldValue);

            final int res = workerPropertyEditor.showDialog(this);

            if (res == JOptionPane.OK_OPTION) {
                final String key = workerPropertyEditor.getKey().toUpperCase();
                final String value = workerPropertyEditor.getValue();

                if (!oldKey.equals(key) && !key.equals(PropertiesConstants.NAME)) {
                    removeProperty(oldKey);
                }

                addOrEditProperty(key, value);
            }
        }
    }//GEN-LAST:event_editPropertyButtonActionPerformed

    private void removePropertyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removePropertyButtonActionPerformed
        final int row = propertiesTable.getSelectedRow();

        if (row != -1) {
            final DefaultTableModel model = (DefaultTableModel) propertiesTable.getModel();

            model.removeRow(row);
        }
    }//GEN-LAST:event_removePropertyButtonActionPerformed

    private void workerNameFieldKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_workerNameFieldKeyTyped
        updateControls();
    }//GEN-LAST:event_workerNameFieldKeyTyped

    private void workerImplementationFieldKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_workerImplementationFieldKeyTyped
        updateControls();
    }//GEN-LAST:event_workerImplementationFieldKeyTyped

    private void setMode(final Mode mode) {
        this.mode = mode;
        updateControls();

        // forget about selected file when going to edit properties mode
        if (mode == Mode.EDIT_MANUALLY) {
            fileSelected = false;
        } else if (filePathTextField.getText().length() > 0) {
            // if going from edit manually back to select file, and a file was already
            // selected, consider that to be selected
            fileSelected = true;
        }
    }

    private void goBackToInitialConfig() {
        ((CardLayout) wizardPanel.getLayout()).show(wizardPanel, "initial");
        stage = Stage.INITIAL_CONFIG;

        updateControls();
    }

    /**
     * Generate properties based on filled-in values the add form.
     * 
     * @return Properties file content to fill the editor
     */
    private String generateProperties() {
        // TODO: merge in previous content from the text editor in the case
        // when the user goes back and changes some values in the form and then
        // back to the editor

        final Properties properties = new Properties();

        final String workerId = ((JTextField) workerIdComboBox.getEditor().getEditorComponent()).getText();
        final String classPath = workerImplementationField.getText();
        final String tokenClassPath = tokenImplementationField.getText();
        final String workerName = workerNameField.getText();
        final String workerPrefix = PropertiesConstants.WORKER_PREFIX + workerId;

        // insert CLASSPATH global property
        properties.setProperty(
                PropertiesConstants.GLOBAL_PREFIX_DOT + workerPrefix + "." + PropertiesConstants.CLASSPATH,
                classPath);

        if (tokenClassPath != null && !tokenClassPath.isEmpty()) {
            // insert SIGNERTOKEN.CLASSPATH global property
            properties.setProperty(PropertiesConstants.GLOBAL_PREFIX_DOT + workerPrefix + "."
                    + PropertiesConstants.SIGNERTOKEN + "." + PropertiesConstants.CLASSPATH, tokenClassPath);
        }

        // insert NAME worker property   
        properties.setProperty(workerPrefix + "." + PropertiesConstants.NAME, workerName);

        // generate additional properties
        final DefaultTableModel model = (DefaultTableModel) propertiesTable.getModel();

        for (int i = 0; i < model.getRowCount(); i++) {
            final String key = (String) model.getValueAt(i, 0);
            final String value = (String) model.getValueAt(i, 1);

            properties.setProperty(workerPrefix + "." + key, value);
        }

        final StringWriter writer = new StringWriter();

        try {
            properties.store(writer, null);
        } catch (IOException e) {
            // ignore
        }

        return writer.getBuffer().toString();
    }

    /**
     * Get a list of worker IDs added and modified by the apply operation.
     * 
     * @return List of worker IDs
     */
    public List<Integer> getModifiedWorkers() {
        return modifiedWorkers;
    }

    @Action(block = Task.BlockingScope.APPLICATION)
    public Task nextAction() {
        final Task result;
        switch (stage) {
        case INITIAL_CONFIG:
            result = createLoadTask(false);
            break;
        case EDIT_PROPERTIES:
            result = createApplyTask();
            break;
        default:
            throw new IllegalArgumentException("Unknown state: " + stage);
        }
        return result;
    }

    private Task createApplyTask() {
        return new ApplyTask(org.jdesktop.application.Application
                .getInstance(org.signserver.admin.gui.SignServerAdminGUIApplication.class));
    }

    private Task createLoadTask(final boolean forceReload) {
        return new LoadTask(org.jdesktop.application.Application
                .getInstance(org.signserver.admin.gui.SignServerAdminGUIApplication.class), forceReload);
    }

    @Action(block = Task.BlockingScope.APPLICATION)
    public Task reloadAction() {
        return createLoadTask(true);
    }

    /** 
     * Task for loading configuration from file or putting together a new 
     * configuration and if successful switching to the next stage.
     */
    private class LoadTask extends org.jdesktop.application.Task<String, Void> {
        private final String filePath;
        private final boolean reload;
        private String conf;

        LoadTask(org.jdesktop.application.Application app, boolean forceReload) {
            // Runs on the EDT.  Copy GUI state that
            // doInBackground() depends on from parameters
            // to NextActionTask fields, here.
            super(app);
            filePath = filePathTextField.getText();
            this.reload = forceReload || mode == Mode.EDIT_MANUALLY || fileSelected;
        }

        @Override
        protected String doInBackground() {
            // Your Task's code here.  This method runs
            // on a background thread, so don't reference
            // the Swing GUI from here.

            // TODO: should later on handle merging manual properties to the
            // properties editor and so on...

            // reload configuration if a new file has been selected or if
            // a worker was add using the form
            if (reload) {
                switch (mode) {
                case LOAD_FROM_FILE:
                    final File file = new File(filePath);

                    try {
                        conf = FileUtils.readFileToString(file);
                    } catch (IOException e) {
                        return "Failed to read file: " + e.getLocalizedMessage();
                    }
                    break;
                case EDIT_MANUALLY:
                    conf = generateProperties();
                    break;
                default:
                    throw new IllegalArgumentException("Unknown mode: " + mode);
                }
            }

            return null;
        }

        @Override
        protected void succeeded(String error) {
            // Runs on the EDT.  Update the GUI based on
            // the result computed by doInBackground().

            if (error == null) {
                stage = Stage.EDIT_PROPERTIES;
                // in the initial config pane, go to the next step
                ((CardLayout) wizardPanel.getLayout()).show(wizardPanel, "editing");

                if (reload) {
                    configurationTextArea.setText(conf);
                    configurationTextArea.setCaretPosition(0);
                    configurationEdited = false;
                    // reset file selected status
                    fileSelected = false;
                }
                updateControls();
            } else {
                JOptionPane.showMessageDialog(AddWorkerDialog.this, error, "Failed to load properties",
                        JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    private class ApplyTask extends org.jdesktop.application.Task<String, Void> {
        private final String conf;

        ApplyTask(org.jdesktop.application.Application app) {
            // Runs on the EDT.  Copy GUI state that
            // doInBackground() depends on from parameters
            // to NextActionTask fields, here.
            super(app);
            conf = configurationTextArea.getText();

        }

        @Override
        protected String doInBackground() {
            // Your Task's code here.  This method runs
            // on a background thread, so don't reference
            // the Swing GUI from here.
            final Properties props = new Properties();
            try {
                props.load(new ByteArrayInputStream(conf.getBytes()));
            } catch (IOException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Error loading properties", e);
                }
                return "Error loading properties: " + e.getMessage();
            }

            final PropertiesParser parser = new PropertiesParser();

            parser.process(props);

            if (parser.hasErrors()) {
                final List<String> errors = parser.getErrors();

                // show the first error message from the parser, to avoid overflowing
                // TODO: maybe add a "more errors..." view later...
                return "Error parsing properties: " + errors.get(0);
            } else {
                final PropertiesApplier applier = new AdminGUIPropertiesApplier();

                applier.apply(parser);

                if (applier.hasError()) {
                    return "Error applying properties: " + applier.getError();
                }

                modifiedWorkers = applier.getWorkerIds();

                try {
                    for (final int workerId : modifiedWorkers) {
                        SignServerAdminGUIApplication.getAdminWS().reloadConfiguration(workerId);
                    }
                } catch (AdminNotAuthorizedException_Exception e) {
                    return "Error reloading workers: " + e.getMessage();
                } catch (SOAPFaultException ex) {
                    return "Error reloading workers: " + ex.getMessage();
                } catch (EJBException ex) {
                    return "Error reloading workers: " + ex.getMessage();
                }
            }

            return null;
        }

        @Override
        protected void succeeded(String error) {
            // Runs on the EDT.  Update the GUI based on
            // the result computed by doInBackground().
            if (error == null) {
                dispose();
            } else {
                JOptionPane.showMessageDialog(AddWorkerDialog.this, error, "Failed apply properties",
                        JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton addPropertyButton;
    private javax.swing.JButton backButton;
    private javax.swing.JButton cancelButton;
    private javax.swing.JLabel configurationLabel;
    private javax.swing.JPanel configurationPanel;
    private javax.swing.JScrollPane configurationScrollPane;
    private javax.swing.JTextArea configurationTextArea;
    private javax.swing.JButton editPropertyButton;
    private javax.swing.JRadioButton editWorkerPropertiesRadioButton;
    private javax.swing.JButton filePathBrowseButton;
    private javax.swing.JTextField filePathTextField;
    private javax.swing.JPanel initialSetupPanel;
    private javax.swing.JLabel invalidWorkerIdStatusLabel;
    private javax.swing.JRadioButton loadFromFileRadioButton;
    private javax.swing.ButtonGroup modeSelectButtonGroup;
    private javax.swing.JButton nextApplyButton;
    private javax.swing.JLabel propertiesLabel;
    private javax.swing.JScrollPane propertiesScrollPanel;
    private javax.swing.JTable propertiesTable;
    private javax.swing.JButton removePropertyButton;
    private javax.swing.JButton resetButton;
    private javax.swing.JTextField tokenImplementationField;
    private javax.swing.JLabel tokenImplementationLabel;
    private javax.swing.JPanel wizardPanel;
    private javax.swing.JComboBox workerIdComboBox;
    private javax.swing.JLabel workerIdLabel;
    private javax.swing.JTextField workerImplementationField;
    private javax.swing.JLabel workerImplementationLabel;
    private javax.swing.JTextField workerNameField;
    private javax.swing.JLabel workerNameLabel;
    // End of variables declaration//GEN-END:variables
}