org.nuclos.client.ui.collect.CollectController.java Source code

Java tutorial

Introduction

Here is the source code for org.nuclos.client.ui.collect.CollectController.java

Source

//Copyright (C) 2010  Novabit Informationssysteme GmbH
//
//This file is part of Nuclos.
//
//Nuclos is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Nuclos is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.
//
//You should have received a copy of the GNU Affero General Public License
//along with Nuclos.  If not, see <http://www.gnu.org/licenses/>.
package org.nuclos.client.ui.collect;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.io.Closeable;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.prefs.Preferences;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JInternalFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.RowSorter.SortKey;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.NullArgumentException;
import org.apache.log4j.Logger;
import org.nuclos.client.common.ClientParameterProvider;
import org.nuclos.client.common.DatasourceBasedCollectableFieldsProvider;
import org.nuclos.client.common.KeyBindingProvider;
import org.nuclos.client.common.MetaDataClientProvider;
import org.nuclos.client.common.NuclosCollectController;
import org.nuclos.client.common.NuclosCollectControllerFactory;
import org.nuclos.client.common.NuclosDropTargetVisitor;
import org.nuclos.client.common.OneDropNuclosDropTargetListener;
import org.nuclos.client.common.WorkspaceUtils;
import org.nuclos.client.entityobject.EntityObjectDelegate;
import org.nuclos.client.genericobject.CollectableGenericObject;
import org.nuclos.client.genericobject.GenericObjectClientUtils;
import org.nuclos.client.genericobject.Modules;
import org.nuclos.client.main.Main;
import org.nuclos.client.main.mainframe.MainFrame;
import org.nuclos.client.main.mainframe.MainFrameTab;
import org.nuclos.client.main.mainframe.MainFrameTabbedPane;
import org.nuclos.client.masterdata.CollectableMasterDataWithDependants;
import org.nuclos.client.searchfilter.SearchFilter;
import org.nuclos.client.ui.CommonAbstractAction;
import org.nuclos.client.ui.CommonClientWorkerAdapter;
import org.nuclos.client.ui.CommonClientWorkerSelfExecutable;
import org.nuclos.client.ui.CommonMultiThreader;
import org.nuclos.client.ui.Errors;
import org.nuclos.client.ui.FrameUtils;
import org.nuclos.client.ui.Icons;
import org.nuclos.client.ui.ListOfValues;
import org.nuclos.client.ui.MainFrameTabAdapter;
import org.nuclos.client.ui.OvOpAdapter;
import org.nuclos.client.ui.OverlayOptionPane;
import org.nuclos.client.ui.ResultListener;
import org.nuclos.client.ui.SimpleDocumentListener;
import org.nuclos.client.ui.TopController;
import org.nuclos.client.ui.UIUtils;
import org.nuclos.client.ui.collect.component.CollectableCheckBox;
import org.nuclos.client.ui.collect.component.CollectableComponent;
import org.nuclos.client.ui.collect.component.CollectableListOfValues;
import org.nuclos.client.ui.collect.component.EntityListOfValues;
import org.nuclos.client.ui.collect.component.ICollectableListOfValues;
import org.nuclos.client.ui.collect.component.model.ChoiceEntityFieldList;
import org.nuclos.client.ui.collect.component.model.DetailsComponentModel;
import org.nuclos.client.ui.collect.component.model.SearchComponentModel;
import org.nuclos.client.ui.collect.detail.DetailsController;
import org.nuclos.client.ui.collect.detail.DetailsPanel;
import org.nuclos.client.ui.collect.model.CollectableTableModel;
import org.nuclos.client.ui.collect.model.SortableCollectableTableModel;
import org.nuclos.client.ui.collect.model.SortableCollectableTableModelImpl;
import org.nuclos.client.ui.collect.result.ResultController;
import org.nuclos.client.ui.collect.result.ResultPanel;
import org.nuclos.client.ui.collect.result.SearchResultStrategy;
import org.nuclos.client.ui.collect.search.ISearchStrategy;
import org.nuclos.client.ui.collect.search.SearchController;
import org.nuclos.client.ui.collect.search.SearchPanel;
import org.nuclos.client.ui.labeled.LabeledComboBox;
import org.nuclos.client.ui.labeled.LabeledDateChooser;
import org.nuclos.client.ui.labeled.LabeledListOfValues;
import org.nuclos.client.ui.labeled.LabeledTextArea;
import org.nuclos.client.ui.labeled.LabeledTextField;
import org.nuclos.client.ui.multiaction.MultiActionProgressPanel;
import org.nuclos.client.ui.table.SortableTableModel;
import org.nuclos.client.ui.table.TableUtils;
import org.nuclos.client.valuelistprovider.cache.CollectableFieldsProviderCache;
import org.nuclos.common.NuclosEOField;
import org.nuclos.common.NuclosFatalException;
import org.nuclos.common.ParameterProvider;
import org.nuclos.common.SpringApplicationContextHolder;
import org.nuclos.common.WorkspaceDescription.EntityPreferences;
import org.nuclos.common.collect.collectable.Collectable;
import org.nuclos.common.collect.collectable.CollectableEntity;
import org.nuclos.common.collect.collectable.CollectableEntityField;
import org.nuclos.common.collect.collectable.CollectableField;
import org.nuclos.common.collect.collectable.CollectableFieldsProvider;
import org.nuclos.common.collect.collectable.CollectableFieldsProviderFactory;
import org.nuclos.common.collect.collectable.CollectableUtils;
import org.nuclos.common.collect.collectable.searchcondition.AtomicCollectableSearchCondition;
import org.nuclos.common.collect.collectable.searchcondition.CollectableIdCondition;
import org.nuclos.common.collect.collectable.searchcondition.CollectableIdListCondition;
import org.nuclos.common.collect.collectable.searchcondition.CollectableSearchCondition;
import org.nuclos.common.collect.collectable.searchcondition.CollectableSubCondition;
import org.nuclos.common.collect.collectable.searchcondition.CompositeCollectableSearchCondition;
import org.nuclos.common.collect.collectable.searchcondition.LogicalOperator;
import org.nuclos.common.collect.collectable.searchcondition.PivotJoinCondition;
import org.nuclos.common.collect.collectable.searchcondition.PlainSubCondition;
import org.nuclos.common.collect.collectable.searchcondition.RefJoinCondition;
import org.nuclos.common.collect.collectable.searchcondition.ReferencingCollectableSearchCondition;
import org.nuclos.common.collect.collectable.searchcondition.SearchConditionUtils;
import org.nuclos.common.collect.collectable.searchcondition.TrueCondition;
import org.nuclos.common.collect.collectable.searchcondition.visit.Visitor;
import org.nuclos.common.collect.exception.CollectableFieldFormatException;
import org.nuclos.common.collect.exception.CollectableValidationException;
import org.nuclos.common.collection.CollectionUtils;
import org.nuclos.common.collection.Transformer;
import org.nuclos.common.dal.vo.EntityFieldMetaDataVO;
import org.nuclos.common2.CommonRunnable;
import org.nuclos.common2.IdUtils;
import org.nuclos.common2.LangUtils;
import org.nuclos.common2.StringUtils;
import org.nuclos.common2.exception.CommonBusinessException;
import org.nuclos.common2.exception.CommonFatalException;
import org.nuclos.common2.exception.CommonFinderException;
import org.nuclos.common2.exception.CommonPermissionException;
import org.nuclos.common2.exception.CommonStaleVersionException;

/**
 * Controller for collecting data (German: "Daten erfassen").
 * Contains the necessary logic to search for, view and edit objects (or database rows).
 * <br>
 * <b>Responsibilities:</b>
 * <ul>
 *   <li>Knows how to select, read, write and delete objects from a persistent store. (Model aspect)</li>
 *   <li>Knows the structure (meta data) of the objects to collect. (Model aspect, CollectableEntity)</li>
 *   <li>Knows how to display and edit objects. (View aspect)</li>
 *   <li>Contains the view, consisting of
 *     <ul>
 *       <li>an JInternalFrame (this is fixed behavior).</li>
 *       <li>a CollectPanel inside the frame.</li>
 *     </ul>
 *   <li>Defines a strategy that is responsible for showing partially loaded objects in the result table
 *       and for completing these objects when they need to be shown completely (in the Details).</li>
 *   <li>Defines a "workflow" consisting of Search, Result and Details with their respective panels, as defined in
 *       CollectPanel.</li>
 *   <li>To be short, has much too much responsibilities...</li>
 * </ul>
 * <br>
 * <br>Created by Novabit Informationssysteme GmbH
 * <br>Please visit <a href="http://www.novabit.de">www.novabit.de</a>
 * @author   <a href="mailto:Christoph.Radig@novabit.de">Christoph.Radig</a>
 * @version 01.00.00
 * TODO try to split up some of the responsibilities in separate classes, but don't sacrifice flexibility, don't make
 * things even more complicated and don't mess up the views with controller code. That's not an easy task...
 */
public abstract class CollectController<Clct extends Collectable> extends TopController
        implements NuclosDropTargetVisitor, Closeable {

    private static final Logger LOG = Logger.getLogger(CollectController.class);

    /**
     * the CollectPanel for this controller
     * TODO Why is this an extra field inside the frame?
     */
    private WeakReference<CollectPanel<Clct>> pnlCollect;

    /**
     * the model used for synchronizing the navigation buttons with the selection in the result table and the result table model.
     */
    private CollectNavigationModel navigationmodel;

    /**
     * the state model that encapsulates the states and transitions for the collecting process.
     * TODO move to CollectPanel
     */
    private CollectStateModel<Clct> statemodel;

    /**
     * the collectable entity for this controller
     */
    private final CollectableEntity clcte;

    private int iLockCount = 0;

    protected boolean bIsLastTabDetailsModeMultiViewOrEdit = false;

    protected MouseListener foreignKeyMouseListenerForTableDoubleClick;

    protected CollectableFieldsProviderCache valueListProviderCache = new CollectableFieldsProviderCache();

    /**
     * @see #isSearchChangedIgnored()
     */
    private boolean bSearchChangedIgnored;

    /**
     * @see #isDetailsChangedIgnored()
     */
    private boolean bDetailsChangedIgnored;

    /**
     * the source of the last invocation of detailsChanged
     */
    private Object oSourceOfLastDetailsChange;

    /**
     * used internally while the frame is closing.
     */
    private boolean bFrameMayBeClosed;

    private List<CollectableEventListener> collectableListeners = new LinkedList<CollectableEventListener>();

    private final SearchController<Clct> ctlSearch = new SearchController<Clct>(this);

    /**
     * TODO: Not every CollectController has a ResultController associated with it.
     *       Hence, this field should appear more 'upward' in the hierarchy.
     *      (Thomas Pasch)
     */
    private final ResultController<Clct> ctlResult;

    private final DetailsController<Clct> ctlDetails = new DetailsController<Clct>(this);

    private ISearchStrategy<Clct> ss;

    private final Map<String, Serializable> context = new HashMap<String, Serializable>();

    private SearchFilter mainFilter;

    private boolean newTabCreated = false;

    private boolean closed = false;

    private InvokeWithInputRequiredSupport invokeWithInputRequiredSupport = null;

    /**
     * Messages for Collectable events
     */
    public static enum MessageType {
        REFRESH_DONE, REFRESH_DONE_DIRECTLY, EDIT_DONE, STATECHANGE_DONE, DELETE_DONE, NEW_DONE, CLCT_LEFT/* extend as needed */
    }

    /**
     * Listener Interface for Collectable events
     */
    public static interface CollectableEventListener {
        public void handleCollectableEvent(Collectable collectable, MessageType messageType);
    }

    CollectableEventListener mandatoryResetEventListener = new CollectableEventListener() {
        @Override
        public void handleCollectableEvent(Collectable collectable, MessageType messageType) {
            switch (messageType) {
            case REFRESH_DONE_DIRECTLY:
            case STATECHANGE_DONE:
            case DELETE_DONE:
            case CLCT_LEFT:
                resetCollectableComponentModelsInDetailsMandatory();
                resetCollectableComponentModelsInDetailsMandatoryAdded();
                break;
            }
        }
    };

    private ActionListener navigationChangeListener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            broadcastCollectableEvent(getSelectedCollectable(), MessageType.CLCT_LEFT);
        }
    };

    protected FocusListener collectableComponentSearchFocusListener = new FocusListener() {
        @Override
        public void focusLost(FocusEvent e) {
        }

        @Override
        public void focusGained(FocusEvent e) {
            CollectController.this.setDefaultButton();
        }
    };

    /**
     * action: New
     */
    private final Action actNew = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.39", "Neu"), Icons.getInstance().getIconNew16(),
            getSpringLocaleDelegate().getMessage("CollectController.23", "Neuen Datensatz erfassen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            askAndSaveIfNecessary(new ResultListener<Boolean>() {
                @Override
                public void done(Boolean result) {
                    if (Boolean.TRUE.equals(result)) {
                        cmdEnterNewMode();
                    }
                }
            });
        }
    };

    /**
     * action: Save
     */
    private final Action actSave = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.40", "Speichern"),
            Icons.getInstance().getIconSave16(), getSpringLocaleDelegate().getMessage("CollectController.6",
                    "\u00c4nderungen an diesem Datensatz speichern")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdSave();
        }
    };

    /**
     * action: Bookmark
     */
    private final Action actBookmark = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.105", "Lesezeichen setzen"),
            Icons.getInstance().getIconBookmark16(), getSpringLocaleDelegate().getMessage("CollectController.106",
                    "Lesezeichen auf den Start Tabs setzen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdBookmarkSelectedCollectable();
        }

        @Override
        public boolean isEnabled() {
            return CollectController.this.getCollectState().isDetailsModeViewOrEdit()
                    || (CollectController.this.getCollectState().isResultMode()
                            && CollectController.this.getSelectedCollectables().size() == 1);
        }

    };

    /**
     * action: Bookmark
     */
    private final Action actCopyCells = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("ResultPanel.13", "Kopiere markierte Zellen"),
            Icons.getInstance().getIconCopy16(),
            getSpringLocaleDelegate().getMessage("ResultPanel.13", "Kopiere markierte Zellen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            UIUtils.copyCells(getResultTable());
        }

    };

    /**
     * action: Bookmark
     */
    private final Action actCopyRows = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("ResultPanel.14", "Kopiere markierte Zeilen"),
            Icons.getInstance().getIconCopy16(),
            getSpringLocaleDelegate().getMessage("ResultPanel.14", "Kopiere markierte Zeilen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            UIUtils.copyRows(getResultTable());
        }

    };

    /**
     * action: Open in new tab
     */
    private final Action actOpenInNewTab = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.107", "In neuem Tab \u00f6ffnen"),
            Icons.getInstance().getIconOpenInNewTab16(),
            getSpringLocaleDelegate().getMessage("CollectController.108", "Details in neuem Tab \u00f6ffnen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdOpenSelectedCollectableInNewTab();
        }

        @Override
        public boolean isEnabled() {
            return CollectController.this.getCollectState().isDetailsModeViewOrEdit()
                    || (CollectController.this.getCollectState().isResultMode());
        }

    };

    /**
     * action: Clone
     */
    private final Action actClone = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.38", "Klonen"),
            Icons.getInstance().getIconClone16(),
            getSpringLocaleDelegate().getMessage("CollectController.7", "Ausgew\u00e4hlten Datensatz klonen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            askAndSaveIfNecessary(new ResultListener<Boolean>() {
                @Override
                public void done(Boolean result) {
                    if (Boolean.TRUE.equals(result)) {
                        cmdCloneSelectedCollectable();
                    }
                }
            });
        }
    };

    /**
     * action: Refresh
     */
    private final Action actRefreshCurrentCollectable = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("CollectController.37", "Aktualisieren"),
            Icons.getInstance().getIconRefresh16(), getSpringLocaleDelegate().getMessage("CollectController.4",
                    "Aktualisieren (Datensatz neu laden und \u00c4nderungen verwerfen)")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdRefreshCurrentCollectable();
        }
    };

    /**
     * action: First
     */
    private final Action actFirst = new CommonAbstractAction(
            MainFrame.resizeAndCacheIcon(Icons.getInstance().getIconFirstWhite16(), DetailsPanel.recordNavIconSize),
            null) { //SpringLocaleDelegate.getMessage("CollectController.33","Zum ersten Datensatz springen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdFirst();
        }
    };

    /**
     * action: Last
     */
    private final Action actLast = new CommonAbstractAction(
            MainFrame.resizeAndCacheIcon(Icons.getInstance().getIconLastWhite16(), DetailsPanel.recordNavIconSize),
            null) { //SpringLocaleDelegate.getMessage("CollectController.34","Zum letzten Datensatz springen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdLast();
        }
    };

    /**
     * action: Previous
     */
    private final Action actPrevious = new CommonAbstractAction(MainFrame.resizeAndCacheIcon(
            Icons.getInstance().getIconPreviousWhite16(), DetailsPanel.recordNavIconSize), null) { //SpringLocaleDelegate.getMessage("CollectController.36","Zum vorigen Datensatz springen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdPrevious();
        }
    };

    /**
     * action: Next
     */
    private final Action actNext = new CommonAbstractAction(
            MainFrame.resizeAndCacheIcon(Icons.getInstance().getIconNextWhite16(), DetailsPanel.recordNavIconSize),
            null) { //SpringLocaleDelegate.getMessage("CollectController.35","Zum n\u00e4chsten Datensatz springen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdNext();
        }
    };

    /**
     * action: Next
     */
    private final Action actResetMainFilter = new CommonAbstractAction(null,
            Icons.getInstance().getIconFilterActive16(),
            getSpringLocaleDelegate().getMessage("CollectController.43", "Filter entfernen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdResetMainFilter();
        }
    };

    protected final DocumentListener documentlistenerDetailsChanged = new SimpleDocumentListener() {
        @Override
        public void documentChanged(DocumentEvent ev) {
            CollectController.this.detailsChanged(ev.getDocument());
        }
    };

    protected final ItemListener itemlistenerDetailsChanged = new ItemListener() {
        @Override
        public void itemStateChanged(ItemEvent ev) {
            CollectController.this.detailsChanged(ev.getSource());
        }
    };

    protected final ChangeListener changelistenerDetailsChanged = new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent ev) {
            CollectController.this.detailsChanged(ev.getSource());
        }
    };

    protected final TableModelListener tblmdllistenerDetailsChanged = new TableModelListener() {
        @Override
        public void tableChanged(TableModelEvent ev) {
            CollectController.this.detailsChanged(ev.getSource());
        }
    };

    protected final ChangeListener changelistenerSearchChanged = new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent ev) {
            CollectController.this.searchChanged(ev.getSource());
        }
    };

    /**
     * constructs a new CollectController.<br>
     * <em>Important: The constructor of the derived controller class must call
     * <ol>
     * <li>setInternalFrame()
     * <li>initialize()
     * </ol>
     * in this order.</em>
     * @param parent
     */
    protected CollectController(CollectableEntity clcte, MainFrameTab tabIfAny, ResultController<Clct> rc) {
        super();
        this.clcte = clcte;
        this.ctlResult = rc;
        this.ctlResult.setCollectController(this);

        this.invokeWithInputRequiredSupport = SpringApplicationContextHolder
                .getBean(InvokeWithInputRequiredSupport.class);

        setTab(tabIfAny);
    }

    /**
     * Don't make this public!
     *
     * @deprecated You should normally do sth. like this:<code><pre>
     * ResultController<~> rc = new ResultController<~>();
     * *CollectController<~> cc = new *CollectController<~>(.., rc);
     * </code></pre>
     */
    protected CollectController(CollectableEntity clcte, MainFrameTab tabIfAny) {
        this(clcte, tabIfAny, new ResultController<Clct>(clcte, new SearchResultStrategy<Clct>()));
    }

    protected WorkspaceUtils getWorkspaceUtils() {
        return WorkspaceUtils.getInstance();
    }

    protected MainFrame getMainFrame() {
        return Main.getInstance().getMainFrame();
    }

    public final ISearchStrategy<Clct> getSearchStrategy() {
        return ss;
    }

    public final void setSearchStrategy(ISearchStrategy<Clct> ss) {
        this.ss = ss;
    }

    /**
     * is entity of this controller transferable, that means that collectables
     * of this entity can be exported and imported. Default is NOT transferable!
     *
     * TODO: Make this protected again.
     */
    public boolean isTransferable() {
        return false;
    }

    /**
     * @deprecated I would wish we could avoid method.
     *       Not every CollectController has a ResultController associated with it.
     *       Hence, this field should appear more 'upward' in the hierarchy.
     */
    public final ResultController<Clct> getResultController() {
        return ctlResult;
    }

    /**
     * @deprecated I would wish we could avoid method.
     *       Not every CollectController has a ResultController associated with it.
     *       Hence, this field should appear more 'upward' in the hierarchy.
     */
    public final ChoiceEntityFieldList getFields() {
        return ctlResult.getFields();
    }

    /**
     * @deprecated Move to SearchController.
     */
    public Collection<SearchComponentModel> getSearchCollectableComponentModels() {
        return this.ctlSearch.getCollectableComponentModels();
    }

    public List<CollectableComponent> getDetailCollectableComponentsFor(String sFieldName) {
        return (List<CollectableComponent>) getDetailsPanel().getEditView().getCollectableComponentsFor(sFieldName);
    }

    /**
     * must be called in constructor of derived classes.
     * TODO refactor!
     * @param pnlCollect the CollectPanel to be used.
     */
    protected void initialize(CollectPanel<Clct> pnlCollect) {
        if (pnlCollect == null) {
            throw new NullPointerException();
        }
        // set the name of the internal frame to this CollectController's entity name (for GUI testing purposes):
        if (this.getTab() != null) {
            this.getTab().setName("ifrm" + StringUtils.capitalized(clcte.getName()));
        }

        this.setCollectPanel(pnlCollect);
        getResultController().initializeFields();
        SortableCollectableTableModel<Clct> model = newResultTableModel();
        getResultController().setModel(model);
        // setup sorted fields and sorting order from preferences
        final List<SortKey> sortKeys = readColumnOrderFromPreferences();
        if (model.getColumnCount() > 0) {
            try {
                model.setSortKeys(sortKeys, false);
            } catch (IllegalArgumentException e) {
                // sortKeys contains invalid column index, ignore
            }
        }
        this.getCollectStateModel().addCollectStateListener(new DefaultCollectStateListener());
        this.getResultPanel().setupTableCellRenderers(getResultTable());
        getResultController().setColumnWidths(getResultTable());
        this.addCollectableEventListener(mandatoryResetEventListener);

        if (this.isSearchPanelAvailable()) {
            this.ctlSearch.setupSearchEditor();
        }

        // TODO all actions that are specific to the Details tab must be disabled here and in detailsModeLeft()
        this.getSaveAction().setEnabled(false);
        setupDragDrop();

        getCollectPanel().getDetailsPanel().addMainFrameTabListener(new MainFrameTabAdapter() {

            @Override
            public void tabClosing(MainFrameTab tab, final ResultListener<Boolean> rl) {
                askAndSaveIfNecessary(true, new ResultListener<Boolean>() {
                    @Override
                    public void done(Boolean result) {
                        rl.done(Boolean.TRUE.equals(result));
                    }
                });
            }

        });
    }

    protected void setupDragDrop() {
        OneDropNuclosDropTargetListener listener = new OneDropNuclosDropTargetListener(this, ClientParameterProvider
                .getInstance().getIntValue(ParameterProvider.KEY_DRAG_CURSOR_HOLDING_TIME, 600));
        DropTarget drop = new DropTarget(this.getResultTable(), listener);
        drop.setActive(true);
    }

    /**
     * @return the user preferences node for this
     */
    public Preferences getPreferences() {
        return this.getUserPreferencesRoot().node("collect").node("entity").node(this.getEntityName());
    }

    /**
     *
     * @return
     */
    public EntityPreferences getEntityPreferences() {
        return getMainFrame().getWorkspaceDescription().getEntityPreferences(this.getEntityName());
    }

    /**
     * @return the application specific user preferences root. Note that this method serves only for the implementation
     *         of getPreferences. Always use getPreferences when you want to store CollectController-specific preferences.
     */
    protected abstract Preferences getUserPreferencesRoot();

    /**
     * @return the "New" action
     *
     * TODO: Move to ResultPanel and make protected again.
     */
    public final Action getNewAction() {
        return this.actNew;
    }

    /**
     * @return the "New with search values" action
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public final Action getNewWithSearchValuesAction() {
        return ctlSearch.getNewWithSearchValuesAction();
    }

    /**
     * @return the "Save" action
     *
     * TODO: Make protected again.
     */
    public final Action getSaveAction() {
        return this.actSave;
    }

    /**
     * @return the "Refresh current collectable" action
     *
     * TODO: Move to DetailsController???
     * TODO: Make protected again.
     */
    public final Action getRefreshCurrentCollectableAction() {
        return this.actRefreshCurrentCollectable;
    }

    /**
     * @return the "Search" action
     *
     * @deprecated Move to SearchController.
     */
    protected final Action getSearchAction() {
        return ctlSearch.getSearchAction();
    }

    /**
     * @return the "Open in new tab" action
     *
     * TODO: Make protected again.
     */
    public final Action getOpenInNewTabAction() {
        return this.actOpenInNewTab;
    }

    /**
     * TODO: Make protected again.
     */
    public final Action getCopyCellsAction() {
        return this.actCopyCells;
    }

    /**
     * TODO: Make protected again.
     */
    public final Action getCopyRowsAction() {
        return this.actCopyRows;
    }

    /**
     * @return the "Bookmark" action
     *
     * TODO: Move to ResultPanel and make protected again.
     */
    public final Action getBookmarkAction() {
        return this.actBookmark;
    }

    /**
     * @return the "Clone" action
     *
     * TODO: Move to ResultPanel and make protected again.
     */
    public final Action getCloneAction() {
        return this.actClone;
    }

    /**
     * @return the "First" action
     *
     * TODO: Make protected again.
     */
    public final Action getFirstAction() {
        return this.actFirst;
    }

    /**
     * @return the "Last" action
     *
     * TODO: Make protected again.
     */
    public final Action getLastAction() {
        return this.actLast;
    }

    /**
     * @return the "Previous" action
     *
     * TODO: Make protected again.
     */
    public final Action getPreviousAction() {
        return this.actPrevious;
    }

    /**
     * @return the "Next" action
     *
     * TODO: Make protected again.
     */
    public final Action getNextAction() {
        return this.actNext;
    }

    /**
     * @return the "Reset Main Filter" action
     *
     * TODO: Make protected again.
     */
    public final Action getResetMainFilterAction() {
        return this.actResetMainFilter;
    }

    /**
     *
     * @param sortImmediately
     */
    public void restoreColumnOrderFromPreferences(boolean sortImmediately) {
        getResultTableModel().setSortKeys(readColumnOrderFromPreferences(), sortImmediately);
    }

    /**
     * Reads the user-preferences for the sorting order.
     */
    protected List<SortKey> readColumnOrderFromPreferences() {
        return readColumnOrderFromPreferences(getResultTableModel());
    }

    /**
     * Reads the user-preferences for the sorting order.
     */
    protected List<SortKey> readColumnOrderFromPreferences(final SortableCollectableTableModel<Clct> tblmdl) {
        return getWorkspaceUtils().getSortKeys(getEntityPreferences(), new WorkspaceUtils.IColumnIndexRecolver() {
            @Override
            public int getColumnIndex(String columnIdentifier) {
                try {
                    return tblmdl.findColumnByFieldName(columnIdentifier);
                } catch (ClassCastException cce) {
                    LOG.error("ResultTableModel is not sortable", cce);
                    return -1;
                }
            }
        });
    }

    /**
     * displays the current search condition in the Search panel's status bar.
     *
     * @deprecated Move to SearchController.
     */
    protected final void cmdDisplayCurrentSearchConditionInSearchPanelStatusBar() {
        UIUtils.runShortCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() {
                ctlSearch.displayCurrentSearchConditionInSearchPanelStatusBar();
            }
        });
    }

    /**
     * @return <code>CollectableFieldsProviderFactory</code> for the search editor. This is used to
     *         display the list of possible values in the dropdown for atomic search nodes.
     *         The default implementation returns null. To enable the search editor, a subclass must return
     *         a valid factory here.
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public CollectableFieldsProviderFactory getCollectableFieldsProviderFactoryForSearchEditor() {
        return null;
    }

    /**
     * @deprecated Move to SearchController and make protected again.
     */
    public Collection<CollectableEntityField> getAdditionalSearchFields() {
        return null;
    }

    /**
     * @return <code>MultiActionProgressPanel</code> for the MultiObjectsActionController.
     *
     * TODO: Make protected again.
     */
    public MultiActionProgressPanel getMultiActionProgressPanel(int iCount) {
        return new MultiActionProgressPanel(iCount);
    }

    /**
     * Locks or unlocks the frame, i.e. makes it (im)possible for the user to trigger any action on it.
     * This is mostly used for background processes
     * @param bLock lock if true, unlock else
     * TODO rename to setFrameLocked
     */
    public void lockFrame(boolean bLock) {
        if (bLock) {
            iLockCount++;
            if (iLockCount > 0) {
                UIUtils.showWaitCursorForFrame(this.getTab(), true);
                CollectController.this.setTitle(getTab().getTitle() + " ("
                        + getSpringLocaleDelegate().getMessage("CollectController.19", "In Bearbeitung") + ")");
            }
        } else {
            iLockCount--;
            if (iLockCount == 0) {
                UIUtils.showWaitCursorForFrame(this.getTab(), false);
                CollectController.this.setTitle();
            }
        }
    }

    /**
     * Remove the lock on a frame (glasspane etc.) forcefully, mostly in case of error
     */
    public void forceUnlockFrame() {
        iLockCount = 0;
        UIUtils.showWaitCursorForFrame(this.getTab(), false);
        CollectController.this.setTitle(getTab().getTitle());
    }

    /**
     * Checks wether user action is prohibited or not, i.e. a background process on this object is running or not.
     * @return
     * TODO rename to isFrameLocked
     * TODO Is this needed at all? It's never used.
     */
    public boolean isLocked() {
        return iLockCount == 0;
    }

    /**
     * @param sTitle
     * @return a new tab for this controller, as specified by <code>newTab()</code>, with the given title.
     */
    protected final MainFrameTab newTab(String sTitle) {
        final MainFrameTab result = newTab();
        result.setTitle(sTitle);
        return result;
    }

    /**
     * TODO refactor
     * @param pnlCollect
     */
    final void setCollectPanel(CollectPanel<Clct> pnlCollect) {
        _setCollectPanel(new WeakReference<CollectPanel<Clct>>(pnlCollect));

        this.statemodel = new CollectStateModel<Clct>(this.getCollectPanel(), this);

        // initialize "New" and "Clone" actions:
        this.getNewAction().setEnabled(CollectController.this.isNewAllowed());
        this.getNewWithSearchValuesAction().setEnabled(false);
        this.getCloneAction().setEnabled(false);

        if (this.isSearchPanelAvailable()) {
            this.ctlSearch.setupSearchPanel();
        }

        this.ctlResult.setupResultPanel();

        this.ctlDetails.setupDetailsPanel();
    }

    /**
     * Command: view the selected Collectable(s).
     */
    public void cmdViewSelectedCollectables() {
        if (this.multipleCollectablesSelected()) {
            this.cmdEnterMultiViewMode();
        } else {
            this.cmdEnterViewMode();
        }
    }

    /**
     * @return Is more than one Collectable (row) selected in the Result table?
     *
     * TODO: move to navigation model? Or ResultController?
     * TODO: Make this protected again.
     */
    public final boolean multipleCollectablesSelected() {
        return this.getResultTable().getSelectedRowCount() > 1;
    }

    /**
     * sets the internal frame to be used
     * @param ifrm
     */
    private void setTab(final MainFrameTab tabIfAny) {
        super.setParent(tabIfAny == null ? newTab() : tabIfAny);
        initTab();
    }

    protected void initTab() {
        // prevent that the frame is closed when changes are pending:
        getTab().addVetoableChangeListener(new VetoableChangeListener() {
            @Override
            public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
                if (evt.getPropertyName().equals(JInternalFrame.IS_CLOSED_PROPERTY)) {
                    final Boolean bOldValue = (Boolean) evt.getOldValue();
                    final Boolean bNewValue = (Boolean) evt.getNewValue();

                    if (bOldValue == Boolean.FALSE && bNewValue == Boolean.TRUE) {
                        // We need bFrameMayBeClosed as a member variable here, as it is set in
                        // cmdFrameClosing and must be checked here.
                        // JInternalFrame.setClosed() first sends a frame closing event, then a vetoable change event.
                        // Note that this is totally weird. See JInternalFrame.setClosed()
                        if (!CollectController.this.bFrameMayBeClosed) {
                            throw new PropertyVetoException("do not close", evt);
                        }
                    }
                }
            }
        });

        // override close behavior:
        getTab().addMainFrameTabListener(new MainFrameTabAdapter() {
            @Override
            public void tabSelected(MainFrameTab tab) {
                setDefaultButton();
            }

            @Override
            public void tabClosing(MainFrameTab tab, final ResultListener<Boolean> rl) {
                askAndSaveIfNecessary(new ResultListener<Boolean>() {
                    @Override
                    public void done(Boolean result) {
                        rl.done(Boolean.TRUE.equals(result));
                    }
                });
            }

            @Override
            public void tabClosed(MainFrameTab tab) {
                CollectController.this.close();
                tab.removeMainFrameTabListener(this);
            }
        });
    }

    protected MainFrameTab newTab() {
        MainFrameTab tab = new MainFrameTab();
        newTabCreated = true;
        return tab;
    }

    /**
     *
     */
    public void init() {
        if (newTabCreated) {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    Main.getInstance().getMainController().initMainFrameTab(CollectController.this, getTab());
                    MainFrame.getPredefinedEntityOpenLocation(getEntityName()).add(getTab());
                }
            });
        }
        getResultPanel().getSearchFilterBar().addItemListener(new MainFilterChangeListener());
    }

    protected void setDefaultButton() {
        if (CollectController.this.getSearchPanel().getRootPane() != null)
            CollectController.this.getSearchPanel().getRootPane()
                    .setDefaultButton(CollectController.this.getSearchPanel().btnSearch);
    }

    public boolean isClosed() {
        return closed;
    }

    /**
     * Called when the internal frame is closed. Releases all resources held by the controller.
     * This is the right place to remove all listeners.
     */
    @Override
    public void close() {
        if (!closed) {
            LOG.debug("close(): " + this);

            // Search panel:
            if (this.isSearchPanelAvailable()) {
                this.ctlSearch.close();
            }

            // Result panel:
            this.ctlResult.close();

            // Details panel:
            this.ctlDetails.close();

            ss = null;
            pnlCollect = null;
            // Partial fix for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7079260
            collectableComponentSearchFocusListener = null;

            closed = true;
        }
    }

    /**
     * @return Does this <code>CollectController</code> have a Search panel?
     */
    public final boolean isSearchPanelAvailable() {
        return this.getCollectPanel().containsSearchPanel();
    }

    /**
     * @return the source of the last invocation of detailsChanged.
     * TODO move to DetailsController
     */
    protected final Object getSourceOfLastDetailsChange() {
        return this.oSourceOfLastDetailsChange;
    }

    /**
     * resets the source of the last invocation of detailsChanged. It is important to do this in order to prevent
     * memory leaks, esp. when using dynamic layouts.
     * TODO move to DetailsController
     */
    protected final void resetSourceOfLastDetailsChange() {
        this.oSourceOfLastDetailsChange = null;
    }

    /**
     * sets the detailsChangedIgnored property.
     * @param bDetailsChangedIgnored
     * @postcondition isDetailsChangedIgnored() == bDetailsChangedIgnored
     * @see #isDetailsChangedIgnored()
     * TODO move to DetailsController
     */
    public final void setDetailsChangedIgnored(boolean bDetailsChangedIgnored) {
        this.bDetailsChangedIgnored = bDetailsChangedIgnored;
    }

    /**
     * @return Is detailsChanged() ignored? If so, detailsChanged() won't be called when the values of
     *         <code>CollectableComponent</code>s change.
     * TODO move to DetailsController
     */
    public final boolean isDetailsChangedIgnored() {
        return this.bDetailsChangedIgnored;
    }

    /**
     * Notification: the details have changed
     * @param oSource the source that caused the change
     *
     * @deprecated Move to DetailsController and make protected again.
     */
    public final void detailsChanged(final Object oSource) {
        if (!isDetailsChangedIgnored()) {
            CollectController.this.ctlDetails.updateStatusBarIfNecessary();

            // this must be run later as the following actions might add or remove collectable component
            // listeners, or other listeners that caused the event to be fired:
            // TODO runCommandLater is critical here - synchronous execution (runCommand) would be desirable!
            UIUtils.runShortCommandLater(getTab(), new CommonRunnable() {
                @Override
                public void run() throws CommonBusinessException {
                    CollectController.this.oSourceOfLastDetailsChange = oSource;
                    switch (statemodel.getDetailsMode()) {
                    case CollectState.DETAILSMODE_VIEW:
                        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_EDIT);
                        break;
                    case CollectState.DETAILSMODE_NEW:
                        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW_CHANGED);
                        break;
                    case CollectState.DETAILSMODE_NEW_SEARCHVALUE:
                        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW_CHANGED);
                        break;
                    case CollectState.DETAILSMODE_MULTIVIEW:
                        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_MULTIEDIT);
                        break;
                    default:
                        // do nothing
                        LOG.debug(
                                "detailsChanged fired in details mode " + getCollectStateModel().getDetailsMode());
                    }
                }
            });
        }
    }

    /**
     * @deprecated Move to DetailsController.
     */
    protected final DetailsComponentModel getDetailsComponentModel(String sFieldName) {
        for (DetailsComponentModel componentModel : CollectController.this.ctlDetails
                .getCollectableComponentModels()) {
            if (componentModel.getFieldName().equals(sFieldName)) {
                return componentModel;
            }
        }

        throw new CommonFatalException("Field with name " + sFieldName + " not found!");
    }

    /**
     * sets the searchChangedIgnored property.
     * @param bSearchChangedIgnored
     * @postcondition isSearchChangedIgnored() == bSearchChangedIgnored
     * @see #isSearchChangedIgnored()
     * TODO move to SearchController
     */
    protected final void setSearchChangedIgnored(boolean bSearchChangedIgnored) {
        this.bSearchChangedIgnored = bSearchChangedIgnored;
    }

    /**
     * @return Is searchChanged() ignored? If so, searchChanged() won't be called when the values of
     *         <code>CollectableComponent</code>s change.
     * TODO move to SearchController
     */
    protected final boolean isSearchChangedIgnored() {
        return this.bSearchChangedIgnored;
    }

    /**
     * Notification: the search have changed
     * @param oSource the source that caused the change
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public final void searchChanged(Object oSource) {
        if (!isSearchChangedIgnored()) {
            // set status bar in Search panel according to the current search condition:
            CollectController.this.cmdDisplayCurrentSearchConditionInSearchPanelStatusBar();

            // "new with search values" action:
            if (oSource != null && isNewAllowed()) {
                getNewWithSearchValuesAction().setEnabled(true);
            }
        }
    }

    /**
     * @return the <code>EditView</code> in the Search panel.
     */
    protected final EditView getSearchEditView() {
        return this.getSearchPanel().getEditView();
    }

    /**
     * @return the <code>EditView</code> in the Details panel.
     */
    protected final EditView getDetailsEditView() {
        return this.getDetailsPanel().getEditView();
    }

    /**
     * @param bSearch true = Search panel, false = Details panel
     * @return the <code>EditView</code> in the Search or Details panel.
     */
    protected final EditView getEditView(boolean bSearch) {
        return bSearch ? this.getSearchEditView() : this.getDetailsEditView();
    }

    /**
     * makes the views of the given components consistent with their models.
     * @param bSearch
     * @param sFieldName
     * @throws CollectableFieldFormatException
     * TODO inline - after reconsidering makeConsistent() vs. stopEditing()
     */
    public final void makeConsistent(boolean bSearch, String sFieldName) throws CollectableFieldFormatException {
        this.getEditView(bSearch).makeConsistent(sFieldName);
    }

    /**
     * makes the views of the given components consistent with their models.
     * @param bSearch
     * @throws CollectableFieldFormatException
     * TODO inline - but @see LeasedObjectCollectController.
     */
    public void makeConsistent(boolean bSearch) throws CollectableFieldFormatException {
        // make the model consistent with the view:
        this.getEditView(bSearch).makeConsistent();
    }

    /**
     * @return Have the change listeners for the Details tab been added?
     * TODO move to DetailsController
     */
    protected final boolean changeListenersForDetailsAdded() {
        return this.ctlDetails.getChangeListenersAdded();
    }

    /**
     * adds the change listeners for the Details tab
     * @precondition !this.changeListenersForDetailsAdded()
     * @postcondition this.changeListenersForDetailsAdded()
     * TODO move to DetailsController
     */
    protected final void addChangeListenersForDetails() {
        this.ctlDetails.addChangeListeners();

        assert this.changeListenersForDetailsAdded();
    }

    /**
     * removes the change listeners for the details tab
     * @postcondition !this.changeListenersForDetailsAdded()
     * TODO move to DetailsController
     */
    protected final void removeChangeListenersForDetails() {
        this.ctlDetails.removeChangeListeners();

        assert !this.changeListenersForDetailsAdded();
    }

    /**
     * adds additional change listeners for the Details tab.
     * Default implementation: do nothing.
     * Derived classes may override this method to add change listeners for additional
     * (non-collectable) components.
     *
     * @deprecated Move to DetailsController and make protected again.
     */
    public void addAdditionalChangeListenersForDetails() {
        // optional - do nothing here
    }

    /**
     * removes additional change listeners for the Details tab.
     * Default implementation: do nothing.
     * Derived classes may override this method to remove change listeners for additional
     * (non-collectable) components.
     *
     * @deprecated Move to DetailsController and make protected again.
     */
    public void removeAdditionalChangeListenersForDetails() {
        // optional - do nothing here
    }

    /**
     * @return Have the change listeners for the Search tab been added?
     *
     * @deprecated Move to SearchController.
     */
    protected final boolean changeListenersForSearchAdded() {
        return this.ctlSearch.getChangeListenersAdded();
    }

    /**
     * adds the change listeners for the Search tab
     * @precondition !this.changeListenersForSearchAdded()
     * @postcondition this.changeListenersForSearchAdded()
     * @deprecated Move to SearchController.
     */
    protected final void addChangeListenersForSearch() {
        this.ctlSearch.addChangeListeners();

        assert this.changeListenersForSearchAdded();
    }

    /**
     * removes the change listeners for the search tab
     * @postcondition !this.changeListenersForSearchAdded()
     *
     * @deprecated Move to SearchController.
     */
    protected final void removeChangeListenersForSearch() {
        this.ctlSearch.removeChangeListeners();

        assert !this.changeListenersForSearchAdded();
    }

    /**
     * adds additional change listeners for the Search tab.
     * Default implementation: do nothing.
     * Derived classes may override this method to add change listeners for additional
     * (non-collectable) components.
     *
     *  @deprecated Move to SearchController
     */
    public void addAdditionalChangeListenersForSearch() {
        // optional - do nothing here
    }

    /**
     * removes additional change listeners for the Search tab.
     * Default implementation: do nothing.
     * Derived classes may override this method to remove change listeners for additional
     * (non-collectable) components.
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public void removeAdditionalChangeListenersForSearch() {
        // optional - do nothing here
    }

    /**
     * the regular entry point: start in Search tab
     */
    public final void runSearch() throws CommonBusinessException {
        this.runSearch(true);
    }

    /**
     * the regular entry point: start in Search tab
     */
    public final void runSearch(boolean selectTab) throws CommonBusinessException {
        this.setCollectState(CollectState.OUTERSTATE_SEARCH, CollectState.SEARCHMODE_UNSYNCHED);

        if (selectTab) {
            this.selectTab();
        }
    }

    public final void runSearchWith(CollectableSearchCondition cond) throws CommonBusinessException {
        this.setCollectState(CollectState.OUTERSTATE_SEARCH, CollectState.SEARCHMODE_UNSYNCHED);
        if (cond != null) {
            this.setCollectableSearchConditionInSearchPanel(cond);
        }
        this.selectTab();
    }

    /**
     * alternative entry point: lookup a <code>Collectable</code> (in a foreign entity).
     */
    public void runLookupCollectable(final ICollectableListOfValues clctlovSource) throws CommonBusinessException {

        // show the internal frame in the front of the modal layer:
        final MainFrameTab ifrm = this.getTab();

        ifrm.setVisible(true);

        if (!clctlovSource.isSearchComponent()) {
            String label = getSpringLocaleDelegate().getMessage("CollectController.41", "Auswahl bernehmen");
            String description = getSpringLocaleDelegate().getMessage("CollectController.42",
                    "Findet die bernahme in einem Unterformular statt werden mittels Mehrfachauswahl zustzliche Datenstze im Unterformular erzeugt.");
            if (clctlovSource instanceof CollectableListOfValues) {
                final CollectableListOfValues clov = (CollectableListOfValues) clctlovSource;
                if (clov.getValueListProvider() instanceof DatasourceBasedCollectableFieldsProvider) {
                    ss.setValueListProviderDatasource(
                            ((DatasourceBasedCollectableFieldsProvider) clov.getValueListProvider())
                                    .getDatasourceVO());
                    ss.setValueListProviderDatasourceParameter(
                            ((DatasourceBasedCollectableFieldsProvider) clov.getValueListProvider())
                                    .getValueListParameter());
                } else if (clov
                        .getValueListProvider() instanceof CollectableFieldsProviderCache.CachingCollectableFieldsProvider) {
                    CollectableFieldsProvider delegate = ((CollectableFieldsProviderCache.CachingCollectableFieldsProvider) clov
                            .getValueListProvider()).getDelegate();
                    if (delegate instanceof DatasourceBasedCollectableFieldsProvider) {
                        ss.setValueListProviderDatasource(
                                ((DatasourceBasedCollectableFieldsProvider) delegate).getDatasourceVO());
                        ss.setValueListProviderDatasourceParameter(
                                ((DatasourceBasedCollectableFieldsProvider) delegate).getValueListParameter());
                    }
                }
            } else if (clctlovSource instanceof EntityListOfValues) {
                label = getSpringLocaleDelegate().getMessage("CollectController.lookup.generation",
                        "Objekte ausw\u00e4hlen");
                description = getSpringLocaleDelegate().getMessage(
                        "CollectController.lookup.generation.description",
                        "Parameterobjekte fr die Objektgenerierung ausw\u00e4hlen.");
            }
            final JMenuItem miPopupApplySelection = new JMenuItem(label);
            miPopupApplySelection.setToolTipText(description);

            getResultPanel().popupmenuRow.removeAll();//.addSeparator();
            getResultPanel().popupmenuRow.add(miPopupApplySelection);
            miPopupApplySelection.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    acceptLookedUpCollectable(clctlovSource);
                    getTab().dispose();
                }
            });
        }

        // remove mouse listener for double click in table:
        getResultPanel().removeDoubleClickMouseListener(this.getMouseListenerForTableDoubleClick());

        // add alternative mouse listener for foreign key lookup:
        foreignKeyMouseListenerForTableDoubleClick = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent ev) {
                if (SwingUtilities.isLeftMouseButton(ev) && ev.getClickCount() == 2) {
                    int iRow = getResultTable().rowAtPoint(ev.getPoint());
                    if (iRow >= 0 && iRow < getResultTable().getRowCount()) {
                        getResultTable().getSelectionModel().setSelectionInterval(iRow, iRow);
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                acceptLookedUpCollectable(clctlovSource);
                                getTab().dispose();
                            }
                        });
                    }
                }
            }
        };

        getResultPanel().addDoubleClickMouseListener(foreignKeyMouseListenerForTableDoubleClick);
        // @see NUCLOS-432
        //getResultTable().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        getResultTable().getActionMap().put(KeyBindingProvider.EDIT_2.getKey(), new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                acceptLookedUpCollectable(clctlovSource);
                getTab().dispose();
            }
        });

        if (this.isSearchPanelAvailable()) {
            if (ss.getValueListProviderDatasource() != null) {
                this.runViewAll();
            } else {
                this.runSearch();
            }
        } else {
            this.runViewAll();
        }

        final Boolean modalLookup = (Boolean) clctlovSource
                .getProperty(ICollectableListOfValues.PROPERTY_MODAL_LOOKUP);
        if (Boolean.TRUE.equals(modalLookup)) {
            JDialog d = new JDialog(Main.getInstance().getMainFrame(), ifrm.getTitle(), true);
            FrameUtils.externalizeIntoWindow(ifrm, d);
            d.pack();
            d.setVisible(true);
        }
    }

    /**
     * alternative entry point: enter new object
     */
    public final void runNew() throws CommonBusinessException {
        this.runNew(true);
    }

    /**
     * alternative entry point: enter new object
     */
    public final void runNew(boolean selectTab) throws CommonBusinessException {
        this.setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW);

        if (selectTab) {
            this.selectTab();
        }
    }

    public final void runNewWith(Clct clct) throws CommonBusinessException {
        this.setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW);
        if (clct != null) {
            this.unsafeFillDetailsPanel(clct);
        }
        this.selectTab();
    }

    /**
     * alternative entry point: view single object in Details
     * @param clct the object to view in Details
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     * @see #readCollectable(Collectable)
     */
    public final void runViewSingleCollectable(Clct clct) {
        runViewSingleCollectable(clct, true);
    }

    /**
     * alternative entry point: view single object in Details
     * @param clct the object to view in Details
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     * @see #readCollectable(Collectable)
     */
    public final void runViewSingleCollectable(Clct clct, boolean bShow) {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        if (!ss.isCollectableComplete(clct)) {
            throw new IllegalArgumentException("clct");
        }
        this.viewSingleCollectable(clct);
        if (bShow)
            this.selectTab();
    }

    /**
     * alternative entry point: view single object in Details.
     * runs the Controller starting in Details view displaying the object with the given id.
     * @param oId
     * @throws CommonBusinessException
     */
    public final void runViewSingleCollectableWithId(Object oId) throws CommonBusinessException {
        this.runViewSingleCollectableWithId(oId, true);
    }

    /**
     * alternative entry point: view single object in Details.
     * runs the Controller starting in Details view displaying the object with the given id.
     * @param oId
     * @throws CommonBusinessException
     */
    public final void runViewSingleCollectableWithId(Object oId, boolean bShow) throws CommonBusinessException {
        runViewSingleCollectable(findCollectableById(getEntityName(), oId), bShow);
    }

    public final void runViewMultipleCollectablesWithIds(List<Object> ids) throws CommonBusinessException {
        getSearchStrategy().setCollectableIdListCondition(new CollectableIdListCondition(ids));
        fillResultPanel(getSearchStrategy().getSearchResult());
        getResultTable().getSelectionModel().addSelectionInterval(0, getResultTable().getRowCount() - 1);
        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_MULTIVIEW);
        selectTab();
    }

    protected void selectTab() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                getTab().setVisible(true);
            }
        });
    }

    /**
     * @param clct
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     */
    private void viewSingleCollectable(Clct clct) {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        if (!ss.isCollectableComplete(clct)) {
            throw new IllegalArgumentException("clct");
        }
        // fill result table:
        this.fillResultPanel(new ArrayList<Clct>(Collections.singletonList(clct)));

        getCollectPanel().getResultPanel().getResultTable().setRowSelectionInterval(0, 0);
        // select the one result row

        this.cmdEnterViewMode();
    }

    /**
     * alternative entry point: view all (in Results tab)
     * TODO refactor using runViewResults
     */
    public final void runViewAll() throws CommonBusinessException {
        this.runViewAll(true);
    }

    /**
     * alternative entry point: view all (in Results tab)
     * TODO refactor using runViewResults
     */
    public final void runViewAll(boolean selectTab) throws CommonBusinessException {
        this.getCollectPanel().setTabbedPaneEnabledAt(CollectState.OUTERSTATE_DETAILS, false);

        this.viewAll();

        this.setCollectState(CollectState.OUTERSTATE_RESULT, CollectState.RESULTMODE_NOSELECTION);

        if (selectTab) {
            this.selectTab();
        }
    }

    /**
     * views all records in the Results tab. Calls search() with initial (or missing) search condition.
     * TODO refactor
     */
    protected void viewAll() throws CommonBusinessException {
        ss.search();
    }

    /**
     * alternative entry point: view all (in Results tab)
     * @precondition this.isSearchPanelAvailable()
     *
     * TODO: Argument should be List<Long>.
     */
    public final void runViewResults(List<? extends Object> oIds) throws CommonBusinessException {
        getResultController().getSearchResultStrategy().viewList(oIds);
        this.setCollectState(CollectState.OUTERSTATE_RESULT, CollectState.RESULTMODE_NOSELECTION);
        this.selectTab();
    }

    /**
     * alternative entry point: view all (in Results tab)
     * @precondition this.isSearchPanelAvailable()
     */
    public final void runViewResults(CollectableSearchCondition cond) throws CommonBusinessException {
        if (!this.isSearchPanelAvailable()) {
            throw new IllegalStateException("this.isSearchPanelAvailable()");
        }
        this.getCollectPanel().setTabbedPaneEnabledAt(CollectState.OUTERSTATE_DETAILS, false);
        this.setCollectableSearchConditionInSearchPanel(cond);
        ss.search();
        this.setCollectState(CollectState.OUTERSTATE_RESULT, CollectState.RESULTMODE_NOSELECTION);
        this.selectTab();
    }

    /**
     * alternative entry point: view search result (in Results tab)
     * @precondition this.isSearchPanelAvailable()
     *
     * @deprecated Move to ResultController.
     */
    public final void runViewResults(final ICollectableListOfValues clctlovSource) throws CommonBusinessException {
        // remove mouse listener for double click in table:
        final JTable tbl = this.getResultTable();
        tbl.removeMouseListener(this.getMouseListenerForTableDoubleClick());

        // add alternative mouse listener for foreign key lookup:
        foreignKeyMouseListenerForTableDoubleClick = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent ev) {
                if (SwingUtilities.isLeftMouseButton(ev) && ev.getClickCount() == 2) {
                    try {
                        final Collectable clctSelected = CollectController.this.getCompleteSelectedCollectable();
                        // TODO assert clctSelected != null ?
                        if (clctSelected != null) {
                            clctlovSource.acceptLookedUpCollectable(clctSelected);

                            // remove the mouse listener after it has done its job:
                            // tbl.removeMouseListener(this);

                            // TODO may whatever mouselistener was installed should be removed from the table in "close()"

                            // Note that Controller.close() is called implicitly here:
                            getTab().dispose();
                        }
                    } catch (Exception ex) {
                        Errors.getInstance().showExceptionDialog(getTab(), ex);
                    }
                }
            }
        };

        tbl.addMouseListener(foreignKeyMouseListenerForTableDoubleClick);
        if (!this.isSearchPanelAvailable()) {
            throw new IllegalStateException("this.isSearchPanelAvailable()");
        }
        this.setCollectableSearchConditionInSearchPanel(clctlovSource.getCollectableSearchCondition());
        this.selectTab();
        getResultController().getSearchResultStrategy().cmdSearch();
        this.getCollectPanel().setTabbedPaneEnabledAt(CollectState.OUTERSTATE_DETAILS, false);
        this.getCollectPanel().setTabbedPaneEnabledAt(CollectState.OUTERSTATE_SEARCH, false);
    }

    /**
     * @param bMakeConsistent make the search component models consistent with their components? That is: transfer the
     * search condition from the view to the model if they differ? This parameter is passed to #getCollectableSearchConditionFromSearchFields.
     * @return the search condition taken from the search fields or the search editor. May be <code>null</code>.
     * @postcondition !this.isSearchPanelAvailable() --> (result == null)
     * @postcondition result == null || result.isSyntacticallyCorrect()
     * @throws CollectableFieldFormatException if the search condition is not syntactically correct.
     * TODO this.isSearchPanelAvailable() should be a precondition here
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public final CollectableSearchCondition getCollectableSearchConditionFromSearchPanel(boolean bMakeConsistent)
            throws CollectableFieldFormatException {
        final CollectableSearchCondition result = !this.isSearchPanelAvailable() ? null
                : (getSearchPanel().isSearchEditorVisible() || this.getImportedSearchCondition() != null
                        ? getMixedSearchCondition(bMakeConsistent)
                        : this.getCollectableSearchConditionFromSearchFields(bMakeConsistent));

        if (result != null && !result.isSyntacticallyCorrect()) {
            // TODO an InvalidSearchConditionException would be better here
            throw new CollectableFieldFormatException("Die Suchbedingung ist unvollst\u00e4ndig.");
        }

        LangUtils.implies(!this.isSearchPanelAvailable(), result == null);
        assert result == null || result.isSyntacticallyCorrect();
        return result;
    }

    private CollectableSearchCondition importedSearchCondition = null;

    protected void setImportedSearchCondition(CollectableSearchCondition pImportedSearchCondition) {
        this.importedSearchCondition = pImportedSearchCondition;
    }

    protected CollectableSearchCondition getImportedSearchCondition() {
        return this.importedSearchCondition;
    }

    protected void removeImportedSearchConditionWithStatus() {
        setImportedSearchConditionWithStatus(null);
    }

    protected void setImportedSearchConditionWithStatus(CollectableSearchCondition searchCondition) {
        this.setImportedSearchCondition(searchCondition);
        setDisplayMixedSearchConditionForSearchEditor(searchCondition != null);
        cmdDisplayCurrentSearchConditionInSearchPanelStatusBar();
    }

    private CollectableSearchCondition getMixedSearchCondition(boolean bMakeConsistent)
            throws CollectableFieldFormatException {
        CollectableSearchCondition editorSearchCondition = getSearchPanel().getSearchEditorPanel()
                .getSearchCondition();
        CollectableSearchCondition fieldsSearchCondition = this
                .getCollectableSearchConditionFromSearchFields(bMakeConsistent);
        if (this.getImportedSearchCondition() != null) {
            if (editorSearchCondition == null && fieldsSearchCondition == null) {
                return this.getImportedSearchCondition();
            }
            CompositeCollectableSearchCondition mixedSearchCondition = new CompositeCollectableSearchCondition(
                    LogicalOperator.AND);
            mixedSearchCondition.addOperand(this.getImportedSearchCondition());
            setDisplayMixedSearchConditionForSearchEditor(true);
            if (editorSearchCondition != null) {
                mixedSearchCondition.addOperand(editorSearchCondition);
            } else {
                if (fieldsSearchCondition != null) {
                    mixedSearchCondition.addOperand(fieldsSearchCondition);
                }
            }
            return mixedSearchCondition;
        } else {
            return editorSearchCondition;
        }
    }

    /**
     * @deprecated Move to SearchController.
     */
    public void setDisplayMixedSearchConditionForSearchEditor(boolean isMixedSearchCondition) {
        this.ctlSearch.setDisplayMixedSearchCondition(isMixedSearchCondition);
    }

    /**
     * @return the search condition to display (in the status bar). By default, this is the search condition given by
     * getCollectableSearchCondition(), with labels sorted in ascending order. May be <code>null</code>.
     * @throws CollectableFieldFormatException
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public CollectableSearchCondition getCollectableSearchConditionToDisplay()
            throws CollectableFieldFormatException {
        return SearchConditionUtils.sortedByLabels(ss.getCollectableSearchCondition());
    }

    /**
     * sets the given search condition in the search panel.
     * If the search editor is visible, sets the search condition there.
     * Otherwise, tries to set the search condition in the search fields.
     * If the search condition cannot be displayed in the search fields,
     * the search editor is shown and the search condition is set there.
     * @param cond
     * @precondition this.isSearchPanelAvailable()
     * TODO move to SearchPanel
     */
    protected final void setCollectableSearchConditionInSearchPanel(CollectableSearchCondition pCond) {
        if (!this.isSearchPanelAvailable()) {
            throw new IllegalStateException("this.isSearchPanelAvailable");
        }
        CollectableSearchCondition cond = removeAndSetPlainSubCondition(pCond);
        if (cond == null) {
            return;
        }
        if (!getSearchPanel().isSearchEditorVisible() && !getSearchPanel().canDisplayConditionInFields(cond)) {
            getSearchPanel().setSearchEditorVisible(true);
        }
        if (getSearchPanel().isSearchEditorVisible()) {
            getSearchPanel().getSearchEditorPanel().setSortedSearchCondition(cond);
        } else {
            try {
                this.setSearchFieldsAccordingToSearchCondition(cond, true);
            } catch (Exception ex) {
                LOG.info(
                        "Suchbedingung kann nicht in der Maske dargestellt werden. Daher wird der Sucheditor aktiviert.",
                        ex);
                getSearchPanel().setSearchEditorVisible(true);
                getSearchPanel().getSearchEditorPanel().setSortedSearchCondition(cond);
            }
        }
    }

    private CollectableSearchCondition removeAndSetPlainSubCondition(CollectableSearchCondition cond) {
        if (cond instanceof PlainSubCondition) {
            this.setImportedSearchConditionWithStatus(cond);
            return null;
        }
        if (cond instanceof CompositeCollectableSearchCondition) {
            List<CollectableSearchCondition> notPlainOperands = new ArrayList<CollectableSearchCondition>();
            List<CollectableSearchCondition> cOperands = ((CompositeCollectableSearchCondition) cond).getOperands();
            for (CollectableSearchCondition operand : cOperands) {
                if (operand instanceof PlainSubCondition) {
                    this.setImportedSearchCondition(operand);
                } else {
                    notPlainOperands.add(operand);
                }
            }
            if (notPlainOperands.size() > 1) {
                return new CompositeCollectableSearchCondition(
                        ((CompositeCollectableSearchCondition) cond).getLogicalOperator(), notPlainOperands);
            } else {
                return notPlainOperands.get(0);
            }
        }
        return cond;
    }

    /**
     * @param bMakeConsistent make the search component models consistent with their components? That is: transfer the
     * search condition from the view to the model if they differ?
     * @return the search condition contained in the search panel's fields.
     * Subclasses may include non-collectable components and/or subforms here.
     * @precondition this.isSearchPanelAvailable()
     * @postcondition result == null || result.isSyntacticallyCorrect()
     *
     * @deprecated Move to SearchController or SearchPanel and make protected again.
     */
    public CollectableSearchCondition getCollectableSearchConditionFromSearchFields(boolean bMakeConsistent)
            throws CollectableFieldFormatException {
        if (!this.isSearchPanelAvailable()) {
            throw new IllegalStateException("!this.isSearchPanelAvailable()");
        }
        if (bMakeConsistent) {
            this.makeConsistent(true);
        }

        final CollectableSearchCondition result = this.getSearchConditionFromModel();

        assert result == null || result.isSyntacticallyCorrect();

        return result;
    }

    /**
     * @return the search condition contained in the search panel's model.
     * @postcondition !this.isSearchPanelAvailable() --> (result == null)
     * @postcondition result == null || result.isSyntacticallyCorrect()
     */
    protected final CollectableSearchCondition getSearchConditionFromModel() {
        final CollectableSearchCondition result = this.isSearchPanelAvailable()
                ? this.getSearchPanel().getEditModel().getSearchCondition()
                : null;

        LangUtils.implies(!this.isSearchPanelAvailable(), result == null);
        assert result == null || result.isSyntacticallyCorrect();

        return result;
    }

    /**
     * sets the search fields (as opposed to the search editor) according to the given search condition.
     * @param cond
     * @param bClearSearchFields Clear all search fields before setting the search condition?
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public void setSearchFieldsAccordingToSearchCondition(CollectableSearchCondition cond,
            boolean bClearSearchFields) throws CommonBusinessException {
        this._setSearchFieldsAccordingToSearchCondition(cond, bClearSearchFields);
    }

    /**
     * sets the search fields (as opposed to the search editor) according to the given search condition.
     * This is <code>CollectController</code>'s default implementation of <code>setSearchFieldsAccordingToSearchCondition</code>.
     * @param cond
     * @param bClearSearchFields
     * @throws CommonBusinessException
     */
    private void _setSearchFieldsAccordingToSearchCondition(CollectableSearchCondition cond,
            boolean bClearSearchFields) throws CommonBusinessException {
        if (cond == null) {
            this.clearSearchFields();
        } else {
            /** TODO check if the search condition can be displayed in the fields at all,
             * eg. isBasicSearchCondition() - isComplexSearchCondition().
             * For the moment, we assume isBasicSearchCondition(). A basic search condition would be a
             * conjunction, which may be nested. Prohibited are NOT, OR.
             */

            if (bClearSearchFields) {
                // TODO optimize: only clear those search fields that are not contained in the search condition
                this.clearSearchFields();
            }

            cond.accept(new SetSearchFieldsVisitor());
        }
    }

    // TODO refactor - this should not be a method here
    protected void _setSearchFieldsAccordingToSubCondition(CollectableSubCondition subcond)
            throws CommonBusinessException {
        throw new NotImplementedException("subconditions");
    }

    // TODO refactor - this should not be a method here
    protected void _setSearchFieldsAccordingToReferencingSearchCondition(
            ReferencingCollectableSearchCondition refcond) throws CommonBusinessException {
        throw new NotImplementedException("referencing conditions");
    }

    /**
     * inner class SetSearchFieldsVisitor
     */
    private class SetSearchFieldsVisitor implements Visitor<Void, CommonBusinessException> {

        @Override
        public Void visitTrueCondition(TrueCondition truecond) throws CommonBusinessException {
            throw new IllegalArgumentException("truecond");
        }

        @Override
        public Void visitAtomicCondition(AtomicCollectableSearchCondition atomiccond)
                throws CommonBusinessException {
            final String sFieldName = atomiccond.getFieldName();
            final SearchComponentModel clctcompmodel = getSearchPanel().getEditModel()
                    .getCollectableComponentModelFor(sFieldName);
            if (clctcompmodel == null) {
                String sLabel;
                try {
                    sLabel = "mit dem Namen \"" + getCollectableEntity().getEntityField(sFieldName).getLabel()
                            + "\" (interner Name: \"" + sFieldName + "\")";
                } catch (CommonFatalException ex) {
                    sLabel = "mit dem internen Namen \"" + sFieldName + "\"";
                }
                // TODO use more specific exception
                throw new CommonBusinessException("Ein Feld " + sLabel + " ist nicht in der Maske vorhanden.");
            }
            clctcompmodel.setSearchCondition(atomiccond);
            return null;
        }

        @Override
        public Void visitCompositeCondition(CompositeCollectableSearchCondition compositecond)
                throws CommonBusinessException {
            if (compositecond.getLogicalOperator() != LogicalOperator.AND) {
                throw new CommonFatalException(
                        "Dieser logische Operator ist hier nicht erlaubt: " + compositecond.getLogicalOperator());
            }
            for (CollectableSearchCondition condChild : compositecond.getOperands()) {
                CollectController.this._setSearchFieldsAccordingToSearchCondition(condChild, false);
            }
            return null;
        }

        @Override
        public Void visitSubCondition(CollectableSubCondition subcond) throws CommonBusinessException {
            CollectController.this._setSearchFieldsAccordingToSubCondition(subcond);
            return null;
        }

        @Override
        public Void visitPivotJoinCondition(PivotJoinCondition joincond) throws CommonBusinessException {
            // TODO: fix
            // CollectController.this._setSearchFieldsAccordingToSubCondition(joincond);
            return null;
        }

        @Override
        public Void visitRefJoinCondition(RefJoinCondition joincond) throws CommonBusinessException {
            // TODO: fix
            // CollectController.this._setSearchFieldsAccordingToSubCondition(joincond);
            return null;
        }

        @Override
        public Void visitReferencingCondition(ReferencingCollectableSearchCondition refcond)
                throws CommonBusinessException {
            CollectController.this._setSearchFieldsAccordingToReferencingSearchCondition(refcond);
            return null;
        }

        @Override
        public Void visitIdCondition(CollectableIdCondition idcond) throws CommonBusinessException {
            throw new CommonFatalException("Id-Bedingungen k\u00f6nnen nicht in der Suchmaske dargestellt werden.");
        }

        @Override
        public Void visitIdListCondition(CollectableIdListCondition collectableIdListCondition)
                throws CommonBusinessException {
            throw new CommonFatalException("Id-Bedingungen k\u00f6nnen nicht in der Suchmaske dargestellt werden.");
        }

    } // inner class SetSearchFieldsVisitor

    // TODO don't define those keybindings here
    protected void setupShortcutsForTabs(MainFrameTab ifrm) {
        final Action actSelectSearchTab = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent ev) {
                if (getCollectPanel().isTabbedPaneEnabledAt(CollectPanel.TAB_SEARCH)) {
                    getCollectPanel().setTabbedPaneSelectedComponent(getSearchPanel());
                }
            }
        };

        final Action actSelectResultTab = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent ev) {
                if (getCollectPanel().isTabbedPaneEnabledAt(CollectPanel.TAB_RESULT)) {
                    getCollectPanel().setTabbedPaneSelectedComponent(getResultPanel());
                }
            }
        };

        final Action actSelectDetailsTab = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent ev) {
                if (getCollectPanel().isTabbedPaneEnabledAt(CollectPanel.TAB_DETAILS)) {
                    getCollectPanel().setTabbedPaneSelectedComponent(getDetailsPanel());
                }
            }
        };

        final String sKeySelectSearchTab = "SelectSearchTab";
        ifrm.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), sKeySelectSearchTab);
        ifrm.getRootPane().getActionMap().put(sKeySelectSearchTab, actSelectSearchTab);

        final String sKeySelectResultTab = "SelectResultTab";
        ifrm.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0), sKeySelectResultTab);
        ifrm.getRootPane().getActionMap().put(sKeySelectResultTab, actSelectResultTab);

        /**
         * inner class SelectTabAction
         */
        class SelectTabAction extends AbstractAction {

            private int iDirection;

            /**
             * @param iDirection -1 for previous tab, +1 for next tab
             */
            SelectTabAction(int iDirection) {
                this.iDirection = iDirection;
            }

            @Override
            public void actionPerformed(ActionEvent ev) {
                int iExternalTabIndex = getCollectPanel().getTabbedPaneSelectedIndex();

                // try to find the next enabled tab in the given direction.
                // If the selected tab is the only enabled tab, do nothing:
                for (int i = 0; i < getCollectPanel().getTabCount() - 1; ++i) {
                    int iInternalTabIndex = getCollectPanel().getTabIndexOf(iExternalTabIndex);
                    iInternalTabIndex = (iInternalTabIndex + iDirection + getCollectPanel().getTabCount())
                            % getCollectPanel().getTabCount();
                    iExternalTabIndex = getCollectPanel().getExternalTabIndexOf(iInternalTabIndex);
                    if (getCollectPanel().isTabbedPaneEnabledAt(iExternalTabIndex)) {
                        getCollectPanel().setTabbedPaneSelectedIndex(iExternalTabIndex);
                        break;
                    }
                }
            }
        } // inner class SelectTabAction

        final String sKeySelectPreviousTab = "SelectPreviousTab";
        ifrm.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.ALT_DOWN_MASK), sKeySelectPreviousTab);
        ifrm.getRootPane().getActionMap().put(sKeySelectPreviousTab, new SelectTabAction(-1));

        final String sKeySelectNextTab = "SelectNextTab";
        ifrm.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK), sKeySelectNextTab);
        ifrm.getRootPane().getActionMap().put(sKeySelectNextTab, new SelectTabAction(+1));
    }

    /**
     * Command: the frame is closing.
     */
    protected void cmdFrameClosing() {
        if (this.stopEditingInDetails()) {
            this.askAndSaveIfNecessary(new ResultListener<Boolean>() {
                @Override
                public void done(Boolean result) {
                    bFrameMayBeClosed = Boolean.TRUE.equals(result);
                    if (bFrameMayBeClosed) {
                        getTab().dispose();
                    }
                }
            });
        }
    }

    /**
     * command: switch to New mode
     */
    protected void cmdEnterNewMode() {
        UIUtils.runCommand(this.getTab(), new Runnable() {
            @Override
            public void run() {
                enterNewMode();
            }
        });
    }

    /**
     * command: switch to New mode with search values
     *
     * @deprecated Move to SearchController and make protected again.
     */
    public void cmdEnterNewModeWithSearchValues() {
        UIUtils.runCommand(this.getTab(), new Runnable() {
            @Override
            public void run() {
                enterNewModeWithSearchValues();
            }
        });
    }

    /**
     * command: Bookmark selected Collectable
     */
    protected void cmdBookmarkSelectedCollectable() {
        UIUtils.runCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() throws CommonBusinessException {
                bookmarkSelectedCollectable();
            }
        });
    }

    protected void bookmarkSelectedCollectable() throws CommonBusinessException {
        Main.getInstance().getMainFrame().addBookmark(getEntityName(), (Integer) getSelectedCollectableId(),
                getLabelForStartTab());
    }

    /**
     * command: Open selected Collectable in new tab
     */
    protected void cmdOpenSelectedCollectableInNewTab() {
        this.getTab().lockLayerBusy();
        openSelectedCollectableInNewTab();
    }

    protected void openSelectedCollectableInNewTab() {
        String entity = getEntityName();
        final MainFrameTabbedPane openInTabbed;
        if (MainFrame.isPredefinedEntityOpenLocationSet(entity))
            openInTabbed = MainFrame.getPredefinedEntityOpenLocation(entity);
        else
            openInTabbed = MainFrame.getTabbedPane(CollectController.this.getTab());

        final List<Clct> selectedList = getSelectedCollectables();
        final List<Thread> loadingThreads = new ArrayList<Thread>();

        final int openQuestionCount = 5;
        final int size = selectedList.size();

        for (int i = 0; i < size; i++) {
            final int currentIndex = i;
            final Object id = selectedList.get(i).getId();

            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    UIUtils.invokeOnDispatchThread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                if (currentIndex == openQuestionCount && size >= openQuestionCount * 2) {
                                    int res = JOptionPane.showConfirmDialog(openInTabbed.getComponentPanel(),
                                            getSpringLocaleDelegate().getMessage("CollectController.openInNewTab.1",
                                                    "Es wurden bereits {0} Tabs geffnet. Mchten Sie die weiteren {1} Tabs auch noch ffnen?",
                                                    openQuestionCount, (size - openQuestionCount)),
                                            getSpringLocaleDelegate().getMessage("CollectController.openInNewTab.2",
                                                    "Wirklich alle selektierten Datenstze in neuen Tabs ffnen?"),
                                            JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);

                                    if (res == JOptionPane.NO_OPTION) {
                                        getTab().unlockLayer();
                                        loadingThreads.clear();
                                        return;
                                    }
                                }

                                MainFrameTab tab = new MainFrameTab();
                                openInTabbed.add(tab);

                                NuclosCollectController<?> clct = NuclosCollectControllerFactory.getInstance()
                                        .newCollectController(getEntityName(), tab, getCustomUsage());
                                getMainController().initMainFrameTab(clct, tab);
                                tab.postAdd();

                                clct.runViewSingleCollectableWithId(id, false);

                                loadingThreads.remove(0);
                                if (loadingThreads.size() > 0) {
                                    loadingThreads.get(0).start();
                                } else {
                                    getTab().unlockLayer();
                                }
                            } catch (Exception e) {
                                throw new NuclosFatalException(e);
                            }
                        }
                    });
                }
            }, "CollectController.openSelectedCollectableInNewTab");
            loadingThreads.add(t);
        }

        if (loadingThreads.size() > 0) {
            loadingThreads.get(0).start();
        } else {
            this.getTab().unlockLayer();
        }
    }

    /**
     * command: Clone selected Collectable
     */
    protected void cmdCloneSelectedCollectable() {
        UIUtils.runCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() throws CommonBusinessException {
                cloneSelectedCollectable();
            }
        });
    }

    /**
     * switches to "New" mode and fills the Details panel with the contents of the selected Collectable.
     * @throws CommonBusinessException
     */
    protected void cloneSelectedCollectable() throws CommonBusinessException {
        final Clct clctBackup = this.getCompleteSelectedCollectable();
        this.enterNewMode();
        this.unsafeFillDetailsPanel(clctBackup);
    }

    /**
     * switches to New mode
     */
    private void enterNewMode() {
        broadcastCollectableEvent(getSelectedCollectable(), MessageType.CLCT_LEFT);
        if (this.stopEditingInDetails()) {
            try {
                this.setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW);
            } catch (CommonBusinessException ex) {
                Errors.getInstance().showExceptionDialog(this.getTab(), ex);
            }
        }
    }

    /**
     * switches to New mode with search values
     */
    private void enterNewModeWithSearchValues() {
        if (this.stopEditingInDetails()) {
            try {
                this.setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_NEW_SEARCHVALUE);
            } catch (CommonBusinessException ex) {
                Errors.getInstance().showExceptionDialog(this.getTab(), ex);
            }
        }
    }

    /**
     * command: switch to View mode
     */
    protected void cmdEnterViewMode() {
        UIUtils.runCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() throws CommonBusinessException {
                enterViewMode();
            }
        });
    }

    /**
     * switches to View mode
     */
    private void enterViewMode() throws CommonBusinessException {
        if (!isReadAllowed(getSelectedCollectable())) {
            throw new CommonBusinessException("Lesen des Datensatzes nicht erlaubt.");
        }
        this.setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_VIEW);
    }

    protected void cmdEnterMultiViewMode() {
        UIUtils.runCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() throws CommonBusinessException {
                enterMultiViewMode();
            }
        });
    }

    private void enterMultiViewMode() throws CommonBusinessException {
        if (!isReadAllowed(getSelectedCollectables())) {
            throw new CommonBusinessException(
                    "Mindestens einer der ausgew\u00e4hlten Datens\u00e4tze darf nicht gelesen werden.");
        }
        setCollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_MULTIVIEW);
    }

    /**
     * @return the selected <code>Collectable</code>, if any, from the table model.
     * Note that there is no selected <code>Collectable</code> in New mode.
     * Note that the result might be incomplete, that means, some fields might be missing.
     * TODO consider inlining or renaming this method. It's too easy to forget that the result might be incomplete.
     *
     * @deprecated Use getResultController().getSelectedCollectableFromTableModel()
     */
    protected final Clct getSelectedCollectable() {
        return getResultController().getSelectedCollectableFromTableModel();
    }

    /**
     * @return the id of the selected <code>Collectable</code>, if any, from the table model.
     */
    public final Object getSelectedCollectableId() {
        final Collectable clctSelected = this.getSelectedCollectable();
        return (clctSelected == null ? null : clctSelected.getId());
    }

    /**
     * the complete selected <code>Collectable</code>, if any. If the currently selected Collectable in the table model isn't complete
     * (that is: partially loaded), the <code>Collectable</code> is reloaded completely and written back to the table model, before it is returned.
     * Therefore, consecutive calls won't reload the returned <code>Collectable</code> again.
     * Note that this method has the side-effect of replacing the selected Collectable in the ResultTableModel.
     * @postcondition (result != null) --> isCollectableComplete(result)
     * @throws CommonBusinessException
     */
    protected final Clct getCompleteSelectedCollectable() throws CommonBusinessException {
        return getCompleteSelectedCollectable(false);
    }

    /**
     * same as <code>getCompleteSelectedCollectable()</code> but with option to load the collectable without dependants
     * @param blnWithoutDependants
     * @return
     * @throws CommonBusinessException
     */
    protected final Clct getCompleteSelectedCollectable(boolean blnWithoutDependants)
            throws CommonBusinessException {
        final Clct clct = getResultController().getSelectedCollectableFromTableModel();
        final Clct result;
        if (clct == null || ss.isCollectableComplete(clct)) {
            result = clct;
        } else {
            result = this.readCollectable(clct, blnWithoutDependants);
            getResultController().replaceSelectedCollectableInTableModel(result);
        }
        assert !(result != null) || ss.isCollectableComplete(result);
        // Note that the postcondition "result == this.getSelectedCollectableFromTableModel()" is not possible for some
        // implementations of the CollectController that use an UnmodifiableListAdapter around a ProxyList for the TableModel.
        return result;
    }

    /**
     * @return the selected (possibly incomplete) <code>Collectable</code>s.
     * @postcondition result != null
     * @see #isCollectableComplete(Collectable)
     */
    public final List<Clct> getSelectedCollectables() {
        final List<Integer> lstSelectedRowNumbers = CollectionUtils.asList(this.getResultTable().getSelectedRows());
        final List<Clct> result = CollectionUtils.transform(lstSelectedRowNumbers,
                new Transformer<Integer, Clct>() {
                    @Override
                    public Clct transform(Integer iRowNo) {
                        return getResultTableModel().getCollectable(iRowNo);
                    }
                });
        assert result != null;
        return result;
    }

    /**
     * @return the selected <code>Collectable</code>s, all of which are complete.
     * @postcondition result != null
     * @see #isCollectableComplete(Collectable)
     */
    public final List<Clct> getCompleteSelectedCollectables() throws CommonBusinessException {
        final List<Clct> result = new ArrayList<Clct>(getSearchStrategy().getCompleteCollectablesStrategy()
                .getCompleteCollectables(this.getSelectedCollectables(), getCustomUsage()));
        getResultController().replaceCollectablesInTableModel(result);

        assert result != null;
        return result;
    }

    /**
     * command: refresh current <code>Collectable</code>
     * TODO this action is "overloaded". It's at least refresh and cancel in one...
     */
    protected void cmdRefreshCurrentCollectable() {
        LOG.debug("START cmdRefreshCurrentCollectable");
        assert this.statemodel.getOuterState() == CollectState.OUTERSTATE_DETAILS;

        // try to get changes from table cell editors:
        // Ignore the result - refresh is always performed, even if stopEditingInDetails fails
        this.stopEditingInDetails();

        UIUtils.runShortCommand(this.getTab(), new CommonRunnable() {
            @Override
            public void run() {
                try {
                    final int iDetailsMode = statemodel.getDetailsMode();

                    switch (iDetailsMode) {
                    case CollectState.DETAILSMODE_VIEW:
                        refreshCurrentCollectable();
                        broadcastCollectableEvent(getSelectedCollectable(), MessageType.REFRESH_DONE_DIRECTLY);
                        break;

                    case CollectState.DETAILSMODE_EDIT:
                        askAndSaveIfNecessary(new ResultListener<Boolean>() {
                            @Override
                            public void done(Boolean result) {
                                if (Boolean.TRUE.equals(result)) {
                                    try {
                                        refreshCurrentCollectable();
                                        broadcastCollectableEvent(getSelectedCollectable(),
                                                MessageType.REFRESH_DONE_DIRECTLY);
                                    } catch (CommonBusinessException e) {
                                        final String sErrorMsg = "Der Datensatz konnte nicht neu geladen werden.";
                                        Errors.getInstance().showExceptionDialog(getTab(), sErrorMsg, e);
                                    }

                                }
                            }
                        });
                        break;

                    case CollectState.DETAILSMODE_NEW_CHANGED:
                        askAndSaveIfNecessary(new ResultListener<Boolean>() {
                            @Override
                            public void done(Boolean result) {
                                if (Boolean.TRUE.equals(result)) {
                                    enterNewMode();
                                }
                            }
                        });
                        break;

                    case CollectState.DETAILSMODE_MULTIVIEW:
                    case CollectState.DETAILSMODE_MULTIEDIT:
                        askAndSaveIfNecessary(new ResultListener<Boolean>() {
                            @Override
                            public void done(Boolean result) {
                                if (Boolean.TRUE.equals(result)) {
                                    try {
                                        enterMultiViewMode();
                                    } catch (CommonBusinessException e) {
                                        final String sErrorMsg = "Der Datensatz konnte nicht neu geladen werden.";
                                        Errors.getInstance().showExceptionDialog(getTab(), sErrorMsg, e);
                                    }
                                }
                            }
                        });
                        break;

                    default:
                        assert false;
                    }
                } catch (CommonBusinessException ex) {
                    final String sErrorMsg = "Der Datensatz konnte nicht neu geladen werden.";
                    Errors.getInstance().showExceptionDialog(getTab(), sErrorMsg, ex);
                }
            }
        });
        LOG.debug("FINISHED cmdRefreshCurrentCollectable");
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public void refreshCurrentCollectable() throws CommonBusinessException {
        refreshCurrentCollectable(true);
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public void refreshCurrentCollectable(boolean withMultiThreader) throws CommonBusinessException {
        assert this.statemodel.getOuterState() == CollectState.OUTERSTATE_DETAILS;
        assert CollectState.isDetailsModeViewOrEdit(statemodel.getDetailsMode());

        final CommonClientWorkerSelfExecutable clientWorker = new CommonClientWorkerAdapter<Clct>(
                CollectController.this) {
            private Clct clct;

            @Override
            public void init() throws CommonBusinessException {
                if (!CollectController.this.isRefreshSelectedCollectableAllowed()) {
                    throw new CommonPermissionException("Aktualisieren ist nicht erlaubt.");
                }
            }

            @Override
            public void work() throws CommonBusinessException {
                // clear cache before refreshing
                valueListProviderCache.clear();
                // reread the selected object from the database:
                clct = CollectController.this.readSelectedCollectable();
            }

            @Override
            public void paint() throws CommonBusinessException {
                if (clct == null)
                    return;

                // replace the selected object in the result list:
                getResultController().replaceSelectedCollectableInTableModel(clct);

                CollectController.this.enterViewMode();

                broadcastCollectableEvent(clct, MessageType.REFRESH_DONE);
            }
        };

        if (withMultiThreader) {
            CommonMultiThreader.getInstance().execute(clientWorker);
        } else {
            clientWorker.runInCallerThread();
        }
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public final void refreshCurrentCollectableInResult() throws CommonBusinessException {
        refreshCurrentCollectableInResult(true);
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public final void refreshCurrentCollectableInResult(boolean withMultiThreader) throws CommonBusinessException {
        assert this.statemodel.getOuterState() == CollectState.OUTERSTATE_RESULT;
        assert CollectState.isResultModeSelected(statemodel.getResultMode());

        final CommonClientWorkerSelfExecutable clientWorker = new CommonClientWorkerAdapter<Clct>(
                CollectController.this) {
            private Clct clct;

            @Override
            public void init() throws CommonBusinessException {
                if (!CollectController.this.isRefreshSelectedCollectableAllowed()) {
                    throw new CommonPermissionException("Aktualisieren ist nicht erlaubt.");
                }
            }

            @Override
            public void work() throws CommonBusinessException {
                // clear cache before refreshing
                valueListProviderCache.clear();
                // reread the selected object from the database:
                clct = CollectController.this.readSelectedCollectable();
            }

            @Override
            public void paint() throws CommonBusinessException {
                if (clct == null)
                    return;

                // replace the selected object in the result list:
                getResultController().replaceSelectedCollectableInTableModel(clct);

                broadcastCollectableEvent(clct, MessageType.REFRESH_DONE);
            }
        };

        if (withMultiThreader) {
            CommonMultiThreader.getInstance().execute(clientWorker);
        } else {
            clientWorker.runInCallerThread();
        }
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public final void refreshSelectedCollectablesInResult() throws CommonBusinessException {
        refreshSelectedCollectablesInResult(true);
    }

    /**
     * Get the currently viewed collectable again from the database and display it.
     * This is done in a background thread.
     * @throws CommonBusinessException
     */
    public final void refreshSelectedCollectablesInResult(boolean withMultiThreader)
            throws CommonBusinessException {
        assert this.statemodel.getOuterState() == CollectState.OUTERSTATE_RESULT;
        assert CollectState.isResultModeSelected(statemodel.getResultMode());

        final CommonClientWorkerSelfExecutable clientWorker = new CommonClientWorkerAdapter<Clct>(
                CollectController.this) {
            private List<Clct> clcts;

            @Override
            public void init() throws CommonBusinessException {
                if (!CollectController.this.isRefreshSelectedCollectableAllowed()) {
                    throw new CommonPermissionException("Aktualisieren ist nicht erlaubt.");
                }
            }

            @Override
            public void work() throws CommonBusinessException {
                // clear cache before refreshing
                valueListProviderCache.clear();
                // reread the selected object from the database:
                clcts = CollectController.this.readSelectedCollectables();
            }

            @Override
            public void paint() throws CommonBusinessException {
                if (clcts == null || clcts.isEmpty())
                    return;

                // replace the selected object in the result list:
                getResultController().replaceCollectablesInTableModel(clcts);

                //broadcastCollectableEvent(clct, MessageType.REFRESH_DONE);
            }
        };

        if (withMultiThreader) {
            CommonMultiThreader.getInstance().execute(clientWorker);
        } else {
            clientWorker.runInCallerThread();
        }
    }

    /**
     * registers a CollectableEventListener
     * @param CollectableEventListener
     */
    public final void addCollectableEventListener(CollectableEventListener l) {
        if (!collectableListeners.contains(l)) {
            collectableListeners.add(l);
        }
    }

    /**
     * removes the given CollectableEventListener
     * @param CollectableEventListener
     */
    public final void removeCollectableEventListener(CollectableEventListener l) {
        collectableListeners.remove(l);
    }

    /**
     * fires a handleCollectableEvent for all registered listeners
     * @param Clct
     * @param MessageType
     */
    protected void broadcastCollectableEvent(Clct collectable, MessageType messageType) {
        for (CollectableEventListener l : new ArrayList<CollectableEventListener>(collectableListeners)) {
            l.handleCollectableEvent(collectable, messageType);
        }
    }

    /**
     * (re)reads the <code>Collectable</code> that is selected in the result list from its "data source"
     * (typically the server). Nothing else.
     * @return the current value of the selected Collectable, as returned by the server.
     * TODO make this final when readCollectable is refactored.
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected Clct readSelectedCollectable() throws CommonBusinessException {
        final Clct clct = this.getSelectedCollectable();
        if (clct == null) {
            throw new IllegalStateException("Kein Objekt ausgew\u00e4hlt.");
        }
        final Clct result = this.readCollectable(clct);
        assert result != null;
        assert ss.isCollectableComplete(result);
        return result;
    }

    /**
     * 
     * @return
     * @throws CommonBusinessException
     */
    protected Clct readSelectedCollectableForResult() throws CommonBusinessException {
        final Clct clct = this.getSelectedCollectable();
        if (clct == null) {
            throw new IllegalStateException("Kein Objekt ausgew\u00e4hlt.");
        }

        return findCollectableById(clcte.getName(), clct.getId(), getSearchStrategy().getFields());
    }

    /**
     * (re)reads the <code>Collectable</code> that is selected in the result list from its "data source"
     * (typically the server). Nothing else.
     * @return the current value of the selected Collectable, as returned by the server.
     * TODO make this final when readCollectable is refactored.
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected List<Clct> readSelectedCollectables() throws CommonBusinessException {
        final List<Clct> clcts = this.getSelectedCollectables();
        if (clcts == null || clcts.isEmpty()) {
            throw new IllegalStateException("Kein Objekt ausgew\u00e4hlt.");
        }

        final List<Clct> result = new ArrayList<Clct>(clcts.size());
        for (Clct clct : clcts) {
            clct = this.readCollectable(clct);
            assert clct != null;
            assert ss.isCollectableComplete(clct);
            result.add(clct);
        }
        return result;
    }

    /**
     * (re)reads the given <code>Collectable</code> from its "data source" (typically the server).
     * The default implementation uses {@link #findCollectableById(String, Object)}.
     * Successors shouldn't redefine this method unless they can hold objects of more than one entity
     * (such as "general search").
     * @param clct the Collectable to be read from the server. May be incomplete.
     * @return the complete Collectable, as returned by the server.
     * @precondition clct != null
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    public Clct readCollectable(Clct clct) throws CommonBusinessException {
        return readCollectable(clct, false);
    }

    /**
     * same as <code>readCollectable(Clct clct)</code> but with option to read the collectable without dependants
     * @param clct the Collectable to be read from the server. May be incomplete.
     * @param blnWithoutDependants
     * @return the complete Collectable, as returned by the server.
     * @throws CommonBusinessException
     */
    public Clct readCollectable(Clct clct, boolean blnWithoutDependants) throws CommonBusinessException {
        if (blnWithoutDependants) {
            return this.findCollectableByIdWithoutDependants(this.getEntityName(), clct.getId());
        } else {
            return this.findCollectableById(this.getEntityName(), clct.getId());
        }
    }

    /**
     * finds the Collectable from the given id, fetching it from the underlying data store.
     * @param sEntity
     * @param oId
     * @return the Collectable with the given entity and id.
     * @throws CommonBusinessException
     * @precondition sEntity != null
     * @precondition oId != null
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected abstract Clct findCollectableById(String sEntity, Object oId) throws CommonBusinessException;

    protected abstract Clct findCollectableById(String sEntity, Object oId,
            Collection<EntityFieldMetaDataVO> fields) throws CommonBusinessException;

    /**
     * finds the Collectable from the given id, fetching it from the underlying data store.
     * @param sEntity
     * @param oId
     * @return the Collectable with the given entity and id, but without dependants!
     * @throws CommonBusinessException
     */
    protected abstract Clct findCollectableByIdWithoutDependants(String sEntity, Object oId)
            throws CommonBusinessException;

    /**
     * returns the version of the given entity
     * @param sEntity
     * @param oId
     * @throws CommonBusinessException
     */
    protected Integer getVersionOfCollectableById(String sEntity, Object oId) throws CommonBusinessException {
        return EntityObjectDelegate.getInstance().getVersion(sEntity, IdUtils.toLongId(oId));
    }

    /**
     * TODO: Make protected again.
     */
    public void setCollectState(int iTab, int iMode) throws CommonBusinessException {
        this.statemodel.setCollectState(iTab, iMode);
    }

    /**
     * @return the <code>CollectableEntity</code> of the objects that are to be collected with this controller.
     * There are cases (eg. general search) where those objects have different entities. In these cases, this method
     * returns the most common entity. Note that there must be such a common entity for the <code>CollectController</code>
     * to work properly.
     * @see #getCollectableEntityForDetails()
     * TODO add postcondition result != null
     */
    public CollectableEntity getCollectableEntity() {
        return this.clcte;
    }

    /**
     * @return This default implementation returns <code>this.getCollectableEntity()</code>.
     * There are cases (eg. general search) where the objects have different entities. In these cases, successors
     * must implement this method by returning the entity of the object currently edited in Details mode (EDIT or NEW).
     * @see #getCollectableEntity()
     */
    protected CollectableEntity getCollectableEntityForDetails() {
        return this.getCollectableEntity();
    }

    /**
     * fills the collectable component models in the details panel with the values in <code>clct</code>.
     * This method is safe as to detailsChanged(), that means detailsChanged() will not be called through filling the panel.
     * @param clct
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     * TODO move to DetailsController or DetailsPanel
     */
    protected final void safeFillDetailsPanel(Clct clct) throws CommonBusinessException {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        if (!ss.isCollectableComplete(clct)) {
            throw new IllegalArgumentException("clct");
        }
        final boolean bWasDetailsChangedIgnored = this.isDetailsChangedIgnored();
        this.setDetailsChangedIgnored(true);
        try {
            this.unsafeFillDetailsPanel(clct);
        } finally {
            this.setDetailsChangedIgnored(bWasDetailsChangedIgnored);
            this.highlightMandatory();
        }
    }

    /**
     * mandatory fields are highlighted with a special color.
     */
    protected void highlightMandatory() {
        Set<String> mandatoryfields = new HashSet<String>();
        for (EntityFieldMetaDataVO efMeta : MetaDataClientProvider.getInstance()
                .getAllEntityFieldsByEntity(getEntityName()).values()) {
            if (!efMeta.isNullable()) {
                mandatoryfields.add(efMeta.getField());
            }
        }
        setCollectableComponentModelsInDetailsMandatory(mandatoryfields);
    }

    /**
     * fills the collectable component models in the details panel with the values in <code>clct</code>.
     * This method is unsafe as to the change listeners, that means the change listeners should be removed
     * before calling this method.
     * @param clct
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     * TODO move to DetailsController or DetailsPanel
     */
    protected void unsafeFillDetailsPanel(Clct clct) throws CommonBusinessException {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        if (!ss.isCollectableComplete(clct)) {
            throw new IllegalArgumentException("clct");
        }
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            clctcompmodel.setField(clct.getField(clctcompmodel.getFieldName()));
        }
    }

    /**
     * fills the collectable component models in the details panel with the common values in <code>collclct</code>.
     * This method is safe as to detailsChanged(), that means detailsChanged() will not be called through filling the panel.
     * @param collclct the Collectables to edit.
     * TODO move to DetailsController or DetailsPanel
     */
    protected final void safeFillMultiEditDetailsPanel(Collection<Clct> collclct) throws CommonBusinessException {
        final boolean bWasDetailsChangedIgnored = this.isDetailsChangedIgnored();
        this.setDetailsChangedIgnored(true);
        try {
            this.unsafeFillMultiEditDetailsPanel(collclct);
        } finally {
            this.setDetailsChangedIgnored(bWasDetailsChangedIgnored);
        }
    }

    /**
     * fills the collectable component models in the details panel with the common values in <code>collclct</code>.
     * This method is unsafe as to the change listeners, that means the change listeners should be removed
     * before calling this method.
     * @param collclct the <code>Collectable</code>s to edit.
     * TODO move to DetailsController or DetailsPanel
     */
    protected void unsafeFillMultiEditDetailsPanel(Collection<Clct> collclct) throws CommonBusinessException {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            assert clctcompmodel.isMultiEditable();
            final String sFieldName = clctcompmodel.getFieldName();
            final CollectableField clctfCommonValue = CollectableUtils.getCommonValue(collclct, sFieldName);
            if (clctfCommonValue == null) {
                clctcompmodel.unsetCommonValue();
                clctcompmodel.clear();
            } else {
                clctcompmodel.setCommonValue(clctfCommonValue);
                clctcompmodel.setField(clctfCommonValue);
            }
            clctcompmodel.setValueToBeChanged(false);
        }
    }

    /**
     * calls <code>setMultiEditable(bMultiEditable)</code> on all collectable component models in the Details tab.
     * @param bMultiEditable
     * TODO move to DetailsController or DetailsPanel
     */
    protected void setCollectableComponentModelsInDetailsPanelMultiEditable(boolean bMultiEditable) {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            clctcompmodel.setMultiEditable(bMultiEditable);
        }
    }

    /**
     *
     * @param mandatoryfields
     */
    protected void setCollectableComponentModelsInDetailsMandatory(Set<String> mandatoryfields) {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            if (mandatoryfields.contains(clctcompmodel.getEntityField().getName())) {
                clctcompmodel.setMandatory(true);
            }
        }
    }

    /**
     *
     * @param mandatoryfields
     */
    protected void setCollectableComponentModelsInDetailsMandatoryAdded(Set<String> mandatoryfields) {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            if (mandatoryfields.contains(clctcompmodel.getEntityField().getName())) {
                clctcompmodel.setMandatoryAdded(true);
            }
        }
    }

    protected void resetCollectableComponentModelsInDetailsMandatory() {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            clctcompmodel.setMandatory(false);
        }
    }

    protected void resetCollectableComponentModelsInDetailsMandatoryAdded() {
        for (DetailsComponentModel clctcompmodel : getDetailsPanel().getEditModel()
                .getCollectableComponentModels()) {
            clctcompmodel.setMandatoryAdded(false);
        }
    }

    /**
     * reads the field values contained in the specified edit panel into <code>clct</code>.
     * @param clct
     * @param bSearchTab Read values from Search panel? Otherwise read values from Details panel.
     * @throws CollectableValidationException
     * @precondition clct != null
     * TODO move to SearchPanel / DetailsPanel (or EditView)
     */
    protected void readValuesFromEditPanel(Clct clct, boolean bSearchTab) throws CollectableValidationException {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        for (CollectableComponent clctcomp : this.getEditView(bSearchTab).getCollectableComponents()) {
            final String sFieldName = clctcomp.getFieldName();
            final CollectableField clctf;
            try {
                clctf = clctcomp.getModel().getField();
            } catch (Exception ex) {
                final String sMessage = "Fehler beim Lesen des Felds " + sFieldName + ".";
                throw new CommonFatalException(sMessage, ex);
            }
            clct.setField(sFieldName, clctf);
        }
    }

    /**
     * updates the current <code>Collectable</code>: fills it with the values from the Details panel, validates it
     * and updates it in the database. The following methods are called (in this order):
     * <ol>
     * <it>this.getCollectStateModel().getEditedCollectable()</it>
     * <it>this.readValuesFromEditPanel(Collectable, false)</it>
     * <it>this.validate(Collectable)</it>
     * <it>this.updateCurrentCollectable(Collectable)</it>
     * </ol>
     *
     * tsc: parameter in old signature <code>Clct updateCurrentCollectable(boolean dbUpdate)</code> was
     *      useless, because a lot of business logic in subsequent updateCurrentCollectable()-methods was
     *      skipped by setting the parameter to false.
     *
     * @return the updated <code>Collectable</code>, as returned by the server.
     * @throws CommonBusinessException
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected Clct updateCurrentCollectable() throws CommonBusinessException {
        final boolean bWasDetailsChangedIgnored = this.isDetailsChangedIgnored();
        this.setDetailsChangedIgnored(true);
        try {
            final Clct clctCurrent = this.getCollectStateModel().getEditedCollectable();
            assert clctCurrent != null;
            this.readValuesFromEditPanel(clctCurrent, false);
            this.prepareCollectableForSaving(clctCurrent, this.getCollectableEntityForDetails());
            final Clct result = this.updateCurrentCollectable(clctCurrent);
            assert result != null;
            assert ss.isCollectableComplete(result);
            return result;
        } finally {
            this.setDetailsChangedIgnored(bWasDetailsChangedIgnored);
        }
    }

    /**
     * updates the currently edited <code>Collectable</code> in the database.
     * @param clctCurrent the currently edited <code>Collectable</code>. It is filled from the
     * edit panel and validated before this method is called.
     * @return the updated <code>Collectable</code>, as returned by the server
     * @precondition clctCurrent != null
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected Clct updateCurrentCollectable(Clct clctCurrent) throws CommonBusinessException {
        final Clct result = this.updateCollectable(clctCurrent, null);
        assert result != null;
        assert ss.isCollectableComplete(result);
        return result;
    }

    /**
     * updates the given <code>Collectable</code> in the database.
     * @param clct the <code>Collectable</code> to update.
     * @param oAdditionalData Optional additional data that might be needed for storing, eg. dependant records.
     * @return the updated <code>Collectable</code>, as returned by the server
     * @precondition clct != null
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected abstract Clct updateCollectable(Clct clct, Object oAdditionalData) throws CommonBusinessException;

    /**
     * inserts the currently edited (new) Collectable into the database
     * @param clctNew is filled from the edit panel and validated before this method is called.
     * @return the inserted <code>Collectable</code>, as returned by the server.
     * @precondition clctNew.getId() == null
     * @postcondition result != null;
     * @postcondition isCollectableComplete(result)
     */
    protected abstract Clct insertCollectable(Clct clctNew) throws CommonBusinessException;

    /**
     * creates a new Collectable, fills it with the values from the Details panel, validates it
     * and inserts it in the database. The following methods are called (in this order):
     * <ol>
     * <it>this.newCollectable()</it>
     * <it>this.readValuesFromEditPanel(Collectable, false)</it>
     * <it>this.validate(Collectable)</it>
     * <it>this.insertCollectable(Collectable)</it>
     * </ol>
     * @return the inserted <code>Collectable</code>, as returned by the server.
     * @throws CommonBusinessException
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    private Clct insertCollectable() throws CommonBusinessException {
        final boolean bWasDetailsChangedIgnored = this.isDetailsChangedIgnored();
        this.setDetailsChangedIgnored(true);
        try {
            final Clct clctNew = this.newCollectableWithDefaultValues();
            this.readValuesFromEditPanel(clctNew, false);
            this.prepareCollectableForSaving(clctNew, this.getCollectableEntityForDetails());
            final Clct result = this.insertCollectable(clctNew);
            assert result != null;
            assert ss.isCollectableComplete(result);
            return result;
        } finally {
            this.setDetailsChangedIgnored(bWasDetailsChangedIgnored);
        }
    }

    /**
     * This method is called when a Collectable is about to be saved (inserted or updated), just before it is validated.
     * Default implementation: Do nothing.
     * Successors may change the given <code>Collectable</code> in order to prepare it for saving. A common application
     * is to change Booleans from <code>null</code> to <code>false</code>.
     * @param clctCurrent the object that is about to be saved
     * @param clcteCurrent the <code>CollectableEntity</code> of <code>clctCurrent</code>
     * @precondition clctCurrent != null
     * @precondition isCollectableComplete(clctCurrent)
     * @precondition clcteCurrent != null
     */
    protected void prepareCollectableForSaving(Clct clctCurrent, CollectableEntity clcteCurrent) {
        // do nothing here
    }

    /**
     * validates the given <code>Collectable</code>.
     * This is called before the <code>clct</code> is stored.
     * @param clct
     * @throws CommonBusinessException
     * @precondition clct != null
     * @precondition isCollectableComplete(clct)
     */
    protected void validate(Clct clct) throws CommonBusinessException {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        if (!ss.isCollectableComplete(clct)) {
            throw new IllegalArgumentException("clct");
        }
    }

    /**
     * stops editing in the Details panel.
     * Derived classes may stop editing on fields, TableCellEditors etc. here
     * @return Has the editing been stopped?
     */
    protected boolean stopEditingInDetails() {
        // do nothing here
        return true;
    }

    /**
     * stops editing in the Search panel.
     * Derived classes may stop editing on fields, TableCellEditors etc. here
     * @return Has the editing been stopped?
     *
     * TODO: Make this protected again.
     */
    public boolean stopEditingInSearch() {
        // do nothing here
        return true;
    }

    /**
     * Command: Save
     * TODO add precondition this.getCollectState().isDetailsMode()?
     */
    private void cmdSave() {
        final boolean bWasDetailsChangedIgnored = this.isDetailsChangedIgnored();
        // detailsChanged must be ignored here as stopEditingInDetails might cause detailsChanged to be fired:
        this.setDetailsChangedIgnored(true);
        try {
            if (!this.stopEditingInDetails()) {
                // TODO show error message
            } else {
                final String sMessage1 = getSpringLocaleDelegate().getMessage("CollectController.12",
                        "Der Datensatz konnte nicht gespeichert werden");
                try {
                    try {
                        this.save();
                    } catch (Exception ex) {
                        if (!handleSpecialException(ex)) {
                            throw ex;
                        }
                    }
                } catch (CollectableValidationException ex) {
                    handleCollectableValidationException(ex, sMessage1);
                } catch (CommonPermissionException ex) {
                    final String sErrorMsg = "Sie verf\u00fcgen nicht \u00fcber die ausreichenden Rechte, "
                            + "um diesen Datensatz zu speichern.";
                    Errors.getInstance().showExceptionDialog(this.getTab(), sErrorMsg, ex);
                } catch (CommonStaleVersionException ex) {
                    handleStaleVersionException(ex);
                } catch (CommonBusinessException ex) {
                    try {
                        handleSaveException(ex, sMessage1);
                    } catch (CommonFinderException ex2) {
                        final String sErrorMsg = sMessage1
                                + ", da er zwischenzeitlich von einem anderen Benutzer gel\u00f6scht wurde.";
                        Errors.getInstance().showExceptionDialog(this.getTab(), sErrorMsg, ex2);
                    } catch (CommonBusinessException ex2) {
                        Errors.getInstance().showExceptionDialog(this.getTab(), null, ex2);
                    }
                } catch (Exception ex) {
                    Errors.getInstance().showExceptionDialog(this.getTab(), ex);
                } catch (Error error) {
                    Errors.getInstance().getCriticalErrorHandler().handleCriticalError(this.getTab(), error);
                }
            }
        } finally {
            this.setDetailsChangedIgnored(bWasDetailsChangedIgnored);
        }
    }

    protected boolean handleStaleVersionException(Exception ex) {
        if (!(ex instanceof CommonStaleVersionException)) {
            return false;
        }

        final String sMessage1 = getSpringLocaleDelegate().getMessage("CollectController.12",
                "Der Datensatz konnte nicht gespeichert werden");

        final String sMessage = sMessage1 + ", "
                + "da er zwischenzeitlich von einem anderen Benutzer ge\u00e4ndert wurde.\n"
                + "Sie m\u00fcssen den Datensatz neu laden und Ihre \u00c4nderungen dann erneut durchf\u00fchren.\n\n"
                + getSpringLocaleDelegate().getMessage("CollectController.25",
                        "Soll der Datensatz jetzt neu geladen werden?");
        OverlayOptionPane.showConfirmDialog(getTab(), sMessage,
                getSpringLocaleDelegate().getMessage("CollectController.9", "Datensatz ge\u00e4ndert"),
                OverlayOptionPane.OK_CANCEL_OPTION, new OvOpAdapter() {
                    @Override
                    public void done(int result) {
                        if (result == OverlayOptionPane.OK_OPTION) {
                            try {
                                refreshCurrentCollectable();
                            } catch (CommonBusinessException ex2) {
                                Errors.getInstance().showExceptionDialog(getTab(), ex2);
                            }
                        }
                    }
                });
        return true;
    }

    protected boolean handleSpecialException(Exception ex) {
        if (ex instanceof UserCancelledException) {
            // simply do nothing
            getContext().clear();
            return true;
        }
        return false;
    }

    protected void handleCollectableValidationException(CollectableValidationException ex, String sMessage1) {
        Errors.getInstance().showExceptionDialog(this.getTab(), sMessage1 + ".", ex);

        // set focus to questionable field (if any):
        final CollectableEntityField clctefInvalid = ex.getCollectableEntityField();
        if (clctefInvalid != null) {
            final Collection<CollectableComponent> collclctcomp = getDetailsPanel().getEditView()
                    .getCollectableComponentsFor(clctefInvalid.getName());
            if (!collclctcomp.isEmpty()) {
                final CollectableComponent clctcomp = collclctcomp.iterator().next();
                clctcomp.getControlComponent().requestFocusInWindow();
            }
        }
    }

    /**
     * called to handle a <code>CommonBusinessException</code> occuring in <code>save()</code>.
     * Default implementation: just <code>throw ex</code>. Successors may define custom exception handling here.
     * @param ex
     * @param sMessage1 first part of the message ("record could not be saved")
     * @throws CommonBusinessException
     */
    protected void handleSaveException(CommonBusinessException ex, String sMessage1)
            throws CommonBusinessException {
        if (ex instanceof CollectableValidationException) {
            handleCollectableValidationException((CollectableValidationException) ex, sMessage1);
        } else {
            throw ex;
        }
    }

    /**
     * updates or inserts the record shown in details, depending on the current collect mode.
     * @throws CommonFatalException
     * @throws CommonPermissionException
     * TODO wait cursor in cmdSave
     */
    public void save() throws CommonBusinessException {
        if (!isSaveAllowed()) {
            throw new CommonPermissionException("Speichern ist nicht erlaubt.");
        }
        Clct clct = null;
        try {
            UIUtils.setWaitCursor(this.getTab());

            LOG.debug("START save");
            MessageType mt;
            switch (this.statemodel.getDetailsMode()) {
            case CollectState.DETAILSMODE_EDIT:
                LOG.debug("START save updateCurrentCollectable");
                clct = this.updateCurrentCollectable();
                LOG.debug("FINISHED save updateCurrentCollectable");

                // update the selected collectable in the table model:
                getResultController().replaceSelectedCollectableInTableModel(clct);

                LOG.debug("START save enterViewMode");
                this.enterViewMode();
                LOG.debug("FINISHED save enterViewMode");
                mt = MessageType.EDIT_DONE;
                break;

            case CollectState.DETAILSMODE_NEW_CHANGED:
                clct = this.insertCollectable();

                // jump to view mode:
                this.viewSingleCollectable(clct);
                // CollectController.this.setCollectState(CollectStateModel.OUTERSTATE_DETAILS, CollectStateModel.DETAILSMODE_VIEW);
                /** TODO switching the state isn't enough, we have to show values updated by the server. */
                /** TODO this is not right! Probably should be view(), as in update() (see above) */
                /** TODO the table model must be updated as well. */
                mt = MessageType.NEW_DONE;
                break;

            case CollectState.DETAILSMODE_MULTIEDIT:
                final int iCount = CollectController.this.getResultTable().getSelectedRowCount();
                new UpdateSelectedCollectablesController<Clct>(this).run(getMultiActionProgressPanel(iCount));

                // do nothing else here. UpdateSelectedCollectablesController is executed in its own thread.
                mt = MessageType.EDIT_DONE;
                break;

            default:
                throw new CommonFatalException(
                        "Speichern kann nur bei Bearbeitung, Neueingabe oder Sammelbearbeitung durchgef\u00fchrt werden.");
            }
            broadcastCollectableEvent(clct, mt);
        } catch (CommonBusinessException cbe) {
            if (!handleSpecialException(cbe)) {
                throw cbe;
            }
        } finally {
            this.getTab().setCursor(null);
            LOG.debug("FINISHED save");
        }
    }

    /**
     * @param clct the Collectable that will be updated as part of a multiple update. This method should not alter it.
     * @return additional data (if any) needed for multiple updates (usually, that is data dependant on the given Collectable).
     * The default implementation returns <code>null</code>.
     * @throws CommonBusinessException on errors like validation.
     * TODO move to DetailsController?
     */
    protected Object getAdditionalDataForMultiUpdate(Clct clct) throws CommonBusinessException {
        return null;
    }

    /**
     * deletes the selected <code>Collectable</code> permanently
     * @throws CommonBusinessException
     */
    protected final void deleteSelectedCollectable() throws CommonBusinessException {
        this.deleteCollectable(this.getSelectedCollectable());
    }

    /**
     * deletes the given <code>Collectable</code> permanently.
     * @param clct
     * @throws CommonBusinessException
     * @precondition clct != null
     */
    protected abstract void deleteCollectable(Clct clct) throws CommonBusinessException;

    /**
     * deletes the given <code>Collectable</code>.
     * @throws CommonPermissionException if deletion of the given <code>Collectable</code> is not allowed for the current user.
     * @precondition clct != null
     */
    protected final void checkedDeleteCollectable(Clct clct) throws CommonBusinessException {
        if (!isDeleteAllowed(clct)) {
            throw new CommonPermissionException("L\u00f6schen ist nicht erlaubt.");
        }
        this.deleteCollectable(clct);
        this.getResultTableModel().remove(clct);
        broadcastCollectableEvent(clct, MessageType.DELETE_DONE);
        getResultController().getSearchResultStrategy().refreshResult();
    }

    /**
     * deletes the selected <code>Collectable</code>.
     * @throws CommonPermissionException if deletion of the selected <code>Collectable</code> is not allowed for the current user.
     *
     * TODO: Make this protected again.
     */
    public final void checkedDeleteSelectedCollectable() throws CommonBusinessException {
        Clct collectable = this.getCompleteSelectedCollectable();
        this.checkedDeleteCollectable(collectable);
    }

    /**
     * command: delete current collectable in details<br>
     * Deletes the current collectable in Details mode.
     *
     * @deprecated Move to DetailsController and make private again.
     */
    public void cmdDeleteCurrentCollectableInDetails() {
        assert this.getCollectStateModel().getCollectState()
                .equals(new CollectState(CollectState.OUTERSTATE_DETAILS, CollectState.DETAILSMODE_VIEW));

        if (this.stopEditingInDetails()) {
            final String sMessage = getSpringLocaleDelegate().getMessage("GenericObjectCollectController.74",
                    "Soll der angezeigte Datensatz ({0}) wirklich gel\u00f6scht werden?",
                    getSelectedCollectable().getIdentifierLabel());
            final int iBtn = JOptionPane.showConfirmDialog(
                    this.getTab(), sMessage, getSpringLocaleDelegate()
                            .getMessage("GenericObjectCollectController.26", "Datensatz l\u00f6schen"),
                    JOptionPane.YES_NO_OPTION);

            if (iBtn == JOptionPane.OK_OPTION) {
                UIUtils.runCommand(this.getTab(), new Runnable() {
                    @Override
                    public void run() {
                        cmdDeleteCurrentCollectableInDetailsImpl();
                    }
                });
            }
        }
    }

    /**
     * command: delete current collectable in details<br>
     * Deletes the current collectable in Details mode.
     *
     * @deprecated Move to DetailsController and make private.
     */
    public void cmdDeleteCurrentCollectableInDetailsImpl() {
        try {
            // try to find next or previous object:
            final JTable tblResult = getResultTable();
            final int iSelectedRow = tblResult.getSelectedRow();
            if (iSelectedRow < 0) {
                throw new IllegalStateException();
            }

            final int iNewSelectedRow;
            if (iSelectedRow < tblResult.getRowCount() - 1) {
                // the selected row is not the last row: select the next row
                iNewSelectedRow = iSelectedRow;
            } else if (iSelectedRow > 0) {
                // the selected row is not the first row: select the previous row
                iNewSelectedRow = iSelectedRow - 1;
            } else {
                // the selected row is the single row: don't select a row
                assert tblResult.getRowCount() == 1;
                assert iSelectedRow == 0;
                iNewSelectedRow = -1;
            }

            checkedDeleteSelectedCollectable();
            getSearchStrategy().search(true);

            if (iNewSelectedRow == -1) {
                tblResult.getSelectionModel().clearSelection();
                // switch to new mode:
                getResultController().getSearchResultStrategy().refreshResult();
            } else {
                tblResult.getSelectionModel().setSelectionInterval(iNewSelectedRow, iNewSelectedRow);
                // go into view mode again:
                enterViewMode();
            }
        } catch (CommonPermissionException ex) {
            final String sErrorMessage = getSpringLocaleDelegate().getMessage("GenericObjectCollectController.69",
                    "Sie verf\u00fcgen nicht \u00fcber die ausreichenden Rechte, um diesen Datensatz zu l\u00f6schen.");
            Errors.getInstance().showExceptionDialog(getTab(), sErrorMessage, ex);
        } catch (CommonBusinessException ex) {
            if (!handleSpecialException(ex)) {
                Errors.getInstance().showExceptionDialog(getTab(),
                        getSpringLocaleDelegate().getMessage("GenericObjectCollectController.31",
                                "Der Datensatz konnte nicht gel\u00f6scht werden."),
                        ex);
            }
        }
    }

    /**
     * @return Is multithreading enabled?
     * TODO remove this method when the transition to multithreading is done ;)
     *
     * @deprecated Move to ResultController hierarchy and make protected again.
     *   It would be far better, if the class (hierarchy) would known that search
     *   should be single- or multi-threaded.
     */
    public boolean isMultiThreadingEnabled() {
        return false;
    }

    /**
     * shortcut for <code>this.getCollectPanel().getResultPanel().getResultTable()</code>.
     *
     * @deprecated Move this to ResultController.
     */
    public final JTable getResultTable() {
        return this.getResultPanel().getResultTable();
    }

    /**
     * @deprecated Move this to ResultController.
     */
    protected final MouseListener getMouseListenerForTableDoubleClick() {
        return getResultController().getTableDblClickML();
    }

    protected final MouseListener getForeignKeyMouseListenerForTableDoubleClick() {
        return this.foreignKeyMouseListenerForTableDoubleClick;
    }

    /**
     * @return the table model containing the results of the last search.
     *
     * @deprecated Move this to ResultController and make protected again.
     */
    public final SortableCollectableTableModel<Clct> getResultTableModel() {
        return (SortableCollectableTableModel<Clct>) this.getResultTable().getModel();
    }

    /**
     * This method adds a mouse listener to the table model. Be sure to remove it when the
     * table model is no longer in use.
     * It also sorts the returned TableModel by a given column if declared in preferences.
     *
     * @return a new collectable table model.
     */
    protected SortableCollectableTableModel<Clct> newResultTableModel() {
        final SortableCollectableTableModel<Clct> result = new SortableCollectableTableModelImpl<Clct>(
                getEntityName());
        result.setColumns(getFields().getSelectedFields());

        // setup sorted fields and sorting order from preferences
        List<SortKey> sortKeys = readColumnOrderFromPreferences(result);
        if (result.getColumnCount() > 0) {
            try {
                result.setSortKeys(sortKeys, false);
            } catch (IllegalArgumentException e) {
                // sortKeys contains invalid column index, ignore
            }
        }

        TableUtils.addMouseListenerForSortingToTableHeader(this.getResultTable(), result);
        return result;
    }

    /**
     * fills the result panel with the results from the current search.
     * @param lstclct List<Collectable>: the results from the current search.
     */
    public final void fillResultPanel(List<Clct> lstclct) {
        this.fillResultPanel(lstclct, lstclct.size(), true);
    }

    /**
     * fills the result panel with the results from the current search.
     * @param lstclct List<Collectable>: the results from the current search.
     * @param iTotalNumberOfRecords The total number of records found. If the result was truncated, this is higher
     * than lstclct.size().
     * @precondition iTotalNumberOfRecords >= lstclct.size()
     *
     * TODO: Make this protected again.
     */
    public final void fillResultPanel(final List<Clct> lstclct, int iTotalNumberOfRecords, boolean bSortInitially) {
        if (iTotalNumberOfRecords < lstclct.size()) {
            throw new IllegalArgumentException("iTotalNumberOfRecords");
        }
        final boolean bResultTruncated = (iTotalNumberOfRecords > lstclct.size());

        final SortableCollectableTableModel<Clct> tblmodel = getResultTableModel();
        tblmodel.setCollectables(lstclct);
        if (bSortInitially) {
            tblmodel.sort();
        }

        final JTable tblResult = getResultTable();

        //setColumnWidths(tblResult);

        // set collect navigation model (for navigation buttons):
        if (navigationmodel != null)
            navigationmodel.removeChangeListener(navigationChangeListener);
        navigationmodel = new CollectNavigationModel(tblResult.getModel(), tblResult.getSelectionModel());
        navigationmodel.addChangeListener(navigationChangeListener);

        ctlResult.setStatusBar(tblResult, bResultTruncated, iTotalNumberOfRecords);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (!lstclct.isEmpty()) {
                    tblResult.getSelectionModel().setSelectionInterval(0, 0);
                }
            }
        });
    }

    /**
     * TODO: Make this protected again.
     */
    public void writeColumnOrderToPreferences() {
        TableModel resultTableModel = this.getResultTable().getModel();
        // NUCLEUSINT-1045
        if (resultTableModel instanceof SortableTableModel) {
            getWorkspaceUtils().setSortKeys(getEntityPreferences(),
                    ((SortableTableModel) resultTableModel).getSortKeys(),
                    new WorkspaceUtils.IColumnNameResolver() {
                        @Override
                        public String getColumnName(int iColumn) {
                            return getResultTableModel().getCollectableEntityField(iColumn).getName();
                        }
                    });
        }
    }

    protected CollectNavigationModel getCollectNavigationModel() {
        return this.navigationmodel;
    }

    private void cmdFirst() {
        this.askAndSaveIfNecessary(new ResultListener<Boolean>() {
            @Override
            public void done(Boolean result) {
                if (Boolean.TRUE.equals(result)) {
                    CollectController.this.getCollectNavigationModel().selectFirstElement();
                    cmdEnterViewMode();
                }
            }
        });
    }

    private void cmdLast() {
        this.askAndSaveIfNecessary(new ResultListener<Boolean>() {
            @Override
            public void done(Boolean result) {
                if (Boolean.TRUE.equals(result)) {
                    CollectController.this.getCollectNavigationModel().selectLastElement();
                    cmdEnterViewMode();
                }
            }
        });
    }

    private void cmdPrevious() {
        this.askAndSaveIfNecessary(new ResultListener<Boolean>() {
            @Override
            public void done(Boolean result) {
                if (Boolean.TRUE.equals(result)) {
                    CollectController.this.getCollectNavigationModel().selectPreviousElement();
                    cmdEnterViewMode();
                }
            }
        });
    }

    private void cmdNext() {
        this.askAndSaveIfNecessary(new ResultListener<Boolean>() {
            @Override
            public void done(Boolean result) {
                if (Boolean.TRUE.equals(result)) {
                    CollectController.this.getCollectNavigationModel().selectNextElement();
                    cmdEnterViewMode();
                }
            }
        });
    }

    /**
     * asks the user to save the current record if necessary, so that it can be abandoned afterwards.
     * @return can the action be performed?
     */
    public void askAndSaveIfNecessary(final ResultListener<Boolean> rl) {
        askAndSaveIfNecessary(false, new ResultListener<Boolean>() {
            @Override
            public void done(Boolean result) {
                rl.done(Boolean.TRUE.equals(result));
            }
        });
    }

    /**
     * asks the user to save the current record if necessary, so that it can be abandoned afterwards.
     * @return can the action be performed?
     */
    @Override
    public void askAndSaveIfNecessary(final boolean returnNullIfCancel, final ResultListener<Boolean> rl) {

        if (this.changesArePending()) {
            try {
                MainFrame.setSelectedTab(this.getTab());
            } catch (Exception e) {
                // TODO TABS: Ein Overlay Tab kann der MainFrame noch nicht finden... Quickfix try-catch
                LOG.error(e.getMessage(), e);
            }
            final String sMsg = getSpringLocaleDelegate().getMessage("CollectController.14",
                    "Der Datensatz wurde ge\u00e4ndert.")
                    + "\n"
                    + getSpringLocaleDelegate().getMessage("CollectController.32",
                            "Wenn Sie jetzt nicht speichern, werden diese \u00c4nderungen verloren gehen.")
                    + "\n" + getSpringLocaleDelegate().getMessage("CollectController.20", "Jetzt speichern?");

            OverlayOptionPane.showConfirmDialog(this.getTab(), sMsg,
                    getSpringLocaleDelegate().getMessage("CollectController.10", "Datensatz ge\u00e4ndert"),
                    JOptionPane.YES_NO_CANCEL_OPTION, new OvOpAdapter() {
                        @Override
                        public void done(int result) {

                            boolean bResult = true;

                            if (returnNullIfCancel && result == OverlayOptionPane.CANCEL_OPTION) {
                                rl.done(null);
                                return;
                            }
                            bResult = (result != OverlayOptionPane.CANCEL_OPTION
                                    && result != OverlayOptionPane.CLOSED_OPTION);

                            if (result == OverlayOptionPane.YES_OPTION) {
                                try {
                                    // cmdSave cannot be used here, because it does not throw any non-fatal exceptions, which can be used to prevent closing of the window.
                                    // To solve the problem otherwise, handleSaveExeption is called here, which could provide additional behaviour like focus faulty fields...
                                    switch (CollectController.this.statemodel.getDetailsMode()) {
                                    case CollectState.DETAILSMODE_EDIT:
                                    case CollectState.DETAILSMODE_NEW_CHANGED:
                                    case CollectState.DETAILSMODE_MULTIEDIT:
                                        save();
                                        break;
                                    }
                                } catch (CommonPermissionException ex) {
                                    bResult = false;
                                    final String sMessage = getSpringLocaleDelegate().getMessage(
                                            "CollectController.24",
                                            "Sie verf\u00fcgen nicht \u00fcber die ausreichenden Rechte, um dieses Objekt zu speichern.");
                                    Errors.getInstance().showExceptionDialog(getTab(), sMessage, ex);
                                } catch (CommonBusinessException ex) {
                                    bResult = false;
                                    final String sMessage1 = getSpringLocaleDelegate().getMessage(
                                            "CollectController.13",
                                            "Der Datensatz konnte nicht gespeichert werden");
                                    try {
                                        handleSaveException(ex, sMessage1);
                                    } catch (CommonFinderException ex2) {
                                        final String sErrorMsg = getSpringLocaleDelegate().getMessage(
                                                "CollectController.1",
                                                ", da er zwischenzeitlich von einem anderen Benutzer gel\u00f6scht wurde.");
                                        Errors.getInstance().showExceptionDialog(getTab(), sErrorMsg, ex2);
                                    } catch (CommonBusinessException ex2) {
                                        Errors.getInstance().showExceptionDialog(getTab(), sMessage1 + ".", ex2);
                                    }
                                    //final String sMessage = "Der Datensatz konnte nicht gespeichert werden.";
                                    //Errors.getInstance().showExceptionDialog(this.getFrame(), sMessage, ex);
                                } catch (CommonFatalException ex) {
                                    bResult = false;
                                    final String sMessage = getSpringLocaleDelegate().getMessage(
                                            "CollectController.11",
                                            "Der Datensatz konnte nicht gespeichert werden.");
                                    Errors.getInstance().showExceptionDialog(getTab(), sMessage, ex);
                                }
                            }

                            rl.done(bResult);
                        }
                    });
        } else {
            rl.done(true);
        }

    } // askAndSaveIfNecessary

    /**
     * @return Are user changes pending (need to be saved)?
     * NUCLEUSINT-1159
     */
    public boolean changesArePending() {
        return this.statemodel.changesArePending();
    }

    /**
     * @return Is the "Save" action allowed? Default: true. May be overridden by subclasses.
     * TODO consider giving the current collectable (id) here
     */
    protected boolean isSaveAllowed() {
        return true;
    }

    /**
     * @return Is the "New" action allowed? Default: <code>isSaveAllowed()</code>.
     *         May be overridden by subclasses.
     */
    protected boolean isNewAllowed() {
        return isSaveAllowed();
    }

    /**
     * @return Is the "Clone" action allowed? Default: <code>isNewAllowed()</code>.
     *         May be overridden by subclasses.
     */
    protected boolean isCloneAllowed() {
        return isNewAllowed();
    }

    /**
     * @return Is the "Delete" action allowed? Default: true. May be overridden by subclasses.
     */
    protected boolean isDeleteSelectedCollectableAllowed() {
        return isDeleteAllowed(getSelectedCollectable());
    }

    /**
     * @return Is the "Delete" action for the given Collectable allowed? Default: true. May be overridden by subclasses.
     * @precondition clct != null
     */
    protected boolean isDeleteAllowed(Clct clct) {
        return true;
    }

    /**
     * @return Is the "Read" action for the given Collectable allowed? Default: true. May be overridden by subclasses.
     * @precondition clct != null
     */
    protected boolean isReadAllowed(Clct clct) {
        return true;
    }

    /**
     * @return Is the "Read" action for the given set of Collectables allowed? Default: true. May be overridden by subclasses.
     * @precondition clct != null
     */
    protected boolean isReadAllowed(List<Clct> lsClct) {
        return true;
    }

    /**
     * @return Are the "Navigation" actions allowed? Default: true. May be overridden by subclasses.
     */
    protected boolean isNavigationAllowed() {
        return true;
    }

    /**
     * @return Is viewing/editing multiple Collectables allowed?
     * TODO Default should be false here.
     * TODO Document what needs to be done for implementing MultiEdit in subclasses
     */
    protected boolean isMultiEditAllowed() {
        return true;
    }

    /**
     * @return Is the "Refresh" action allowed for the selected Collectable? Default: true. May be overridden by subclasses.
     */
    protected boolean isRefreshSelectedCollectableAllowed() {
        return true;
    }

    /**
     * @return the CollectStateModel used internally.
     *
     * TODO: Can this be protected? Can this move to SearchController?
     */
    public final CollectStateModel<Clct> getCollectStateModel() {
        return this.statemodel;
    }

    /**
     * @return the current collect state (consisting of an outer and an inner state).
     */
    public CollectState getCollectState() {
        return this.getCollectStateModel().getCollectState();
    }

    /**
     * This method is called by <code>cmdClearSearchCondition</code>, that is when the user clicks
     * the "Clear Search Fields" button. The default implementation calls clearSearchFields()
     * and adjusts the search editor, if visible.
     * TODO move to SearchPanel
     */
    protected void clearSearchCondition() {
        this.clearSearchFields();

        if (this.getSearchPanel().isSearchEditorVisible()) {
            this.getSearchPanel().getSearchEditorPanel().setSearchCondition(null);
        }
    }

    /**
     * clears all search fields, suppressing calls to searchChanged() for each field and calling searchChanged(null)
     * once at the end of the operation.
     * TODO move to SearchPanel
     */
    protected void clearSearchFields() {
        final boolean bWasSearchChangedIgnored = this.isSearchChangedIgnored();
        this.setSearchChangedIgnored(true);
        try {
            this._clearSearchFields();
        } finally {
            this.setSearchChangedIgnored(bWasSearchChangedIgnored);
        }
        // trigger an event for the completed search condition:
        this.searchChanged(null);
    }

    /**
     * clears all search fields.
     * TODO move to SearchPanel
     */
    protected void _clearSearchFields() {
        for (CollectableComponent clctcomp : getSearchPanel().getEditView().getCollectableComponents()) {
            clctcomp.clear();
        }
    }

    /**
     * Command: clear search condition
     *
     * @deprecated Move to SearchController and make private again.
     */
    public void cmdClearSearchCondition() {
        try {
            this.stopEditingInSearch();

            this.removeImportedSearchConditionWithStatus();
            this.clearSearchCondition();

            ctlSearch.getNewWithSearchValuesAction().setEnabled(false);

            // set to unsynched state even if no value was changed:
            this.setCollectState(CollectState.OUTERSTATE_SEARCH, CollectState.SEARCHMODE_UNSYNCHED);
        } catch (Exception ex) {
            Errors.getInstance().showExceptionDialog(this.getTab(), null, ex);
        }
    }

    /**
     * @return the CollectPanel, containing the Search-, Result- and DetailsPanels.
     */
    public final CollectPanel<Clct> getCollectPanel() {
        if (pnlCollect == null) {
            return null;
        }
        final CollectPanel<Clct> result = pnlCollect.get();
        assert result != null || isClosed();
        return result;
    }

    protected final void _setCollectPanel(WeakReference<CollectPanel<Clct>> pnlCollect) {
        if (pnlCollect == null) {
            throw new NullPointerException();
        }
        this.pnlCollect = pnlCollect;
    }

    /**
     * TODO: add precondition this.isSearchPanelAvailable()
     *
     * @deprecated move to ResultController hierarchy.
     */
    public SearchPanel getSearchPanel() {
        final CollectPanel<Clct> panel = getCollectPanel();
        if (panel == null) {
            return null;
        }
        return panel.getSearchPanel();
    }

    /**
     * @deprecated Move this to ResultController.
     */
    public ResultPanel<Clct> getResultPanel() {
        final CollectPanel<Clct> panel = getCollectPanel();
        if (panel == null) {
            return null;
        }
        return panel.getResultPanel();
    }

    /**
     * @deprecated Move this to DetailsController.
     */
    public DetailsPanel getDetailsPanel() {
        final CollectPanel<Clct> panel = getCollectPanel();
        if (panel == null) {
            return null;
        }
        return panel.getDetailsPanel();
    }

    /**
     * @return the (internal) entity name.
     */
    public String getEntityName() {
        return this.getCollectableEntity().getName();
    }

    /**
     * @return the (external) entity name, as presented to the user. It is shown in the title bar.
     */
    protected abstract String getEntityLabel();

    /**
     * @param iTab
     * @param iMode
     * @return the title for the given tab and mode, to display in the title bar
     */
    protected String getTitle(int iTab, int iMode) {
        // TODO move these constants to CollectState
        final String[] asTabs = { "Suche", "Ergebnis", "Details" };
        final String[] asDetailsMode = { "Undefiniert", "Details", "Bearbeiten", "Neueingabe",
                "Neueingabe (Ge\u00e4ndert)", "Sammelbearbeitung", "Sammelbearbeitung (Ge\u00e4ndert)" };

        String sPrefix;
        String sSuffix = "";
        final String sMode;

        switch (iTab) {
        case CollectState.OUTERSTATE_DETAILS:
            sPrefix = this.getEntityLabel();
            sMode = asDetailsMode[iMode];
            if (CollectState.isDetailsModeViewOrEdit(iMode)) {
                final String sIdentifier = this.getSelectedCollectable().getIdentifierLabel();
                if (sIdentifier == null) {
                    throw new CommonFatalException("Identifier == null");
                }
                sPrefix += " \"" + sIdentifier + "\"";
            } else if (CollectState.isDetailsModeMultiViewOrEdit(iMode)) {
                sSuffix = " von " + this.getSelectedCollectables().size() + " Objekten";
            }
            break;
        default:
            sPrefix = this.getEntityLabel();
            sMode = asTabs[iTab];
        }

        return sPrefix + " - " + sMode + sSuffix;
    }

    /**
     * sets the title of this controller's frame depending on the current CollectState.
     */
    protected void setTitle() {
        this.setTitle(this.getCollectState().getOuterState(), this.getCollectState().getInnerState());
    }

    /**
     * sets the title of this controller's frame depending on the given CollectState.
     * @param iTab
     * @param iMode
     */
    final void setTitle(int iTab, int iMode) {
        this.setTitle(this.getTitle(iTab, iMode));
    }

    /**
     * sets the title of this controller's frame.
     * @param sTitle
     */
    protected void setTitle(String sTitle) {
        this.getTab().setTitle(sTitle);
    }

    /**
     * @return the message to display in the status bar when changes im multi edit mode have occured.
     * @precondition this.getCollectStateModel().getCollectState().isDetailsModeMultiViewOrEdit()
     *
     * TODO: inline - after refactoring LOCC.getMultiEditChangeString()
     *
     * TODO: Make this protected again.
     */
    public String getMultiEditChangeString() {
        if (!this.getCollectStateModel().getCollectState().isDetailsModeMultiViewOrEdit()) {
            throw new IllegalStateException();
        }

        return this.getDetailsPanel().getMultiEditChangeMessage();
    }

    /**
     * @return a new <code>Collectable</code>. All fields are set to null values.
     *         To set the fields to their default values, <code>CollectableUtils.setDefaultValues</code> must be called
     *         explicitly afterwards. Alternatively, <code>newCollectableWithDefaultValues()</code> may be used.
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    public abstract Clct newCollectable();

    /**
     * @param currclct
     * @return the current collectable filled with the values which are set in the search panel
     * @throws CommonBusinessException
     *
     * @deprecated Move to SearchController.
     */
    protected Clct newCollectableWithSearchValues(Clct currclct) throws CommonBusinessException {
        Collection<SearchComponentModel> collscm = ctlSearch.getCollectableComponentModels();

        for (SearchComponentModel scm : collscm) {
            List<CollectableComponent> lsclctcomp = (List<CollectableComponent>) getDetailsPanel().getEditView()
                    .getCollectableComponentsFor(scm.getFieldName());

            if (lsclctcomp.isEmpty()) {
                continue;
            }

            // get model of first found component
            DetailsComponentModel dcm = lsclctcomp.get(0).getDetailsModel();

            boolean bSetAllowed = true;
            for (CollectableComponent clctcomp : lsclctcomp) {
                if (!isSetAllowedForClctComponent(clctcomp)) {
                    bSetAllowed = false;
                }
            }

            if (bSetAllowed) {
                dcm.setField(scm.getField());
                currclct.setField(dcm.getFieldName(), scm.getField());
                if (scm.getField().getValue() != null) {
                    detailsChanged(dcm);
                }
            }
        }

        return currclct;
    }

    /**
     * fill subforms of the current collectable with the values which are set
     * in the coresponding search panel subforms
     * @throws CommonBusinessException
     */
    protected void newCollectableWithDependantSearchValues() throws CommonBusinessException {
    }

    /**
     * @param clctcomp - collectablecomponent of the details panel
     * this method should only be used to check whether it is allowed to adopt a value from the
     * search panel to the details panel
     */
    protected boolean isSetAllowedForClctComponent(CollectableComponent clctcomp) {
        boolean bEnabled = true;
        JComponent jcomp = clctcomp.getJComponent();

        if (jcomp instanceof LabeledTextField) {
            if (!((LabeledTextField) jcomp).getTextField().isEditable()) {
                bEnabled = false;
            }
        } else if (jcomp instanceof LabeledComboBox) {
            if (!((LabeledComboBox) jcomp).getJComboBox().isEnabled()) {
                bEnabled = false;
            }
        } else if (jcomp instanceof LabeledListOfValues) {
            if (!((ListOfValues) ((LabeledListOfValues) jcomp).getControlComponent()).getJTextField().isEnabled()) {
                bEnabled = false;
            }
        } else if (jcomp instanceof LabeledTextArea) {
            if (!((LabeledTextArea) jcomp).getJTextArea().isEditable()) {
                bEnabled = false;
            }
        } else if (jcomp instanceof LabeledDateChooser) {
            if (!((LabeledDateChooser) jcomp).getJTextComponent().isEditable()) {
                bEnabled = false;
            }
        } else if (clctcomp instanceof CollectableCheckBox) {
            if (!((CollectableCheckBox) clctcomp).getJCheckBox().isEnabled()) {
                bEnabled = false;
            }
        }
        // fallback - maybe the list above has to be adjusted
        if (!jcomp.isEnabled()) {
            bEnabled = false;
        }

        return bEnabled;
    }

    /**
     * @return a new <code>Collectable</code>, filled with default values (according to the default values defined in its entity).
     * If there are no reasonable default values for a specific entity (esp. masterdata entities), this is the place to do it.
     * @postcondition result != null
     * @postcondition isCollectableComplete(result)
     */
    protected Clct newCollectableWithDefaultValues() {
        final Clct result = CollectController.this.newCollectable();
        CollectableUtils.setDefaultValues(result, CollectController.this.getCollectableEntity());
        assert result != null;
        assert ss.isCollectableComplete(result);
        return result;
    }

    /**
     * makes sure the given list of selected fields is non-empty. If the list is empty, this method adds one field to it.
     * This is to avoid a seemingly empty search result, which might be irritating to the user.
     * @param clcte
     * @param lstclctefSelected
     * @precondition clcte != null
     * @precondition lstclctefSelected != null
     * @postcondition !lstclctefSelected.isEmpty()
     *
     * TODO: Make this protected again.
     */
    public void makeSureSelectedFieldsAreNonEmpty(CollectableEntity clcte,
            List<CollectableEntityField> lstclctefSelected) {
        if (lstclctefSelected.isEmpty()) {

            List<String> fieldNames = new ArrayList<String>();
            try {
                List<EntityFieldMetaDataVO> fields = CollectionUtils.sorted(
                        MetaDataClientProvider.getInstance().getAllEntityFieldsByEntity(clcte.getName()).values(),
                        new Comparator<EntityFieldMetaDataVO>() {
                            @Override
                            public int compare(EntityFieldMetaDataVO o1, EntityFieldMetaDataVO o2) {
                                Integer order1 = (o1.getOrder() == null) ? 0 : o1.getOrder();
                                Integer order2 = (o2.getOrder() == null) ? 0 : o2.getOrder();
                                return order1.compareTo(order2);
                            }
                        });

                for (EntityFieldMetaDataVO efMeta : fields) {
                    if (StringUtils.isNullOrEmpty(efMeta.getCalcFunction())) {
                        fieldNames.add(efMeta.getField());
                    }
                }
            } catch (Exception ex) {
                LOG.warn("No entity fields for entity " + clcte.getName(), ex);
            }
            if (fieldNames.isEmpty()) {
                fieldNames.addAll(clcte.getFieldNames());
            }

            CollectableEntityField sysStateIcon = null;
            CollectableEntityField sysStateNumber = null;
            CollectableEntityField sysStateName = null;

            Set<String> clcteFieldNames = clcte.getFieldNames();
            for (String field : fieldNames) {
                if (!clcteFieldNames.contains(field)) {
                    LOG.warn("Field " + field + " in collectable entity " + clcte.getName() + " does not exists");
                    continue;
                }

                CollectableEntityField clctef = clcte.getEntityField(field);
                boolean select = true;
                if (NuclosEOField.getByField(field) != null) {
                    select = false;
                    switch (NuclosEOField.getByField(field)) {
                    case STATEICON:
                        sysStateIcon = clctef;
                        break;
                    case STATENUMBER:
                        sysStateNumber = clctef;
                        break;
                    case STATE:
                        sysStateName = clctef;
                        break;
                    }
                }
                if (select) {
                    lstclctefSelected.add(clctef);
                }
            }

            if (sysStateIcon != null)
                lstclctefSelected.add(sysStateIcon);
            if (sysStateNumber != null)
                lstclctefSelected.add(sysStateNumber);
            if (sysStateName != null)
                lstclctefSelected.add(sysStateName);

            if (lstclctefSelected.isEmpty()) {
                // show any (the first, random) field:
                if (clcte.getFieldNames().isEmpty()) {
                    throw new CommonFatalException(
                            "Die Entit\u00e4t \"" + clcte.getName() + "\" enth\u00e4lt keine Felder.");
                } else {
                    final String sRandomFieldName = clcte.getFieldNames().iterator().next();
                    lstclctefSelected
                            .add(getResultController().getCollectableEntityFieldForResult(clcte, sRandomFieldName));
                }
            }
        }
    }

    /**
     * used by <code>DefaultCollectStateListener</code>:
     * On entering <code>CollectState.DETAILSMODE_VIEW</code> the collectable would be loaded completely.
     * You can choose with or without denpendants.
     * If your CollectController can lazy loads dependants (performance improvement) your must return true.
     *
     * See also:
     * <code>findCollectableById(String sEntity, Object oId)</code>
     * <code>findCollectableByIdWithoutDependants(String sEntity, Object oId)</code>
     * @return true to disable dependants loading on <code>CollectState.DETAILSMODE_VIEW</code>
     */
    protected abstract boolean isDetailsModeViewLoadingWithoutDependants();

    private class DefaultCollectStateListener extends CollectStateAdapter {

        @Override
        public void searchModeEntered(CollectStateEvent ev) throws CommonBusinessException {
            CollectController.this.addChangeListenersForSearch();
            CollectController.this.cmdDisplayCurrentSearchConditionInSearchPanelStatusBar();
        }

        @Override
        public void searchModeLeft(CollectStateEvent ev) throws CommonBusinessException {
            CollectController.this.removeChangeListenersForSearch();
        }

        @Override
        public void resultModeEntered(final CollectStateEvent ev) throws CommonBusinessException {
            // This should be invoked from the dispatch thread because it works on the model
            if (!EventQueue.isDispatchThread()) {
                LOG.warn("resultModeEntered invoked outside of the AWT dispatch thread");
            }
            // However, at the moment, it is still sometimes triggered from other threads. Especially it
            // is triggered concurrently by MultiActions.
            // The NUCLEUSINT-622 workaround used invokeLater(). But delaying the Runnable leads to other
            // problems (and in rare cases exceptions, cf. NUCLOSINT-850). The reason is simple:
            // The CollectStateEvent parameter encodes view information (e.g. how many rows are selected)
            // but there is no guarantee that this still holds when Runnable is "invoked later".
            // So, new workaround is to perform the action instantly on the EDT (invokeAndWait).
            UIUtils.invokeOnDispatchThread(new Runnable() {
                @Override
                public void run() {
                    int iResultMode = ev.getNewCollectState().getInnerState();
                    boolean bOneRowSelected = (iResultMode == CollectState.RESULTMODE_SINGLESELECTION);
                    boolean bMoreThanOneRowsSelected = (iResultMode == CollectState.RESULTMODE_MULTISELECTION);
                    boolean bRowsSelected = bOneRowSelected || bMoreThanOneRowsSelected;

                    getResultController().getEditSelectedCollectablesAction()
                            .setEnabled(bOneRowSelected || (bMoreThanOneRowsSelected && isMultiEditAllowed()));
                    getCloneAction().setEnabled(bOneRowSelected && isCloneAllowed());
                    getResultController().getDeleteSelectedCollectablesAction()
                            .setEnabled(bRowsSelected && isDeleteSelectedCollectableAllowed());

                    // If the selection changes and we are in the result tab, the details panel is no longer in sync:
                    // TODO alternative: sync details panel when details tab is pressed (let the customer decide).
                    getCollectPanel().setTabbedPaneEnabledAt(CollectState.OUTERSTATE_DETAILS, false);

                    // Give the result table the focus so the user can start scrolling with the arrow keys:
                    getResultTable().requestFocusInWindow();
                }
            });
        }

        @Override
        public void detailsModeEntered(CollectStateEvent ev) throws CommonBusinessException {
            final int iDetailsMode = ev.getNewCollectState().getInnerState();

            switch (iDetailsMode) {
            case CollectState.DETAILSMODE_VIEW:
                CollectController.this.setCollectableComponentModelsInDetailsPanelMultiEditable(false);
                Clct clct = CollectController.this.getCompleteSelectedCollectable(
                        CollectController.this.isDetailsModeViewLoadingWithoutDependants());
                CollectController.this.safeFillDetailsPanel(clct);
                if (clct != null && clct.getId() != null && clct.getId() instanceof Integer) {
                    String label = getLabelForStartTab();
                    Main.getInstance().getMainFrame().addHistory(getEntityName(), (Integer) clct.getId(), label);
                }
                break;

            case CollectState.DETAILSMODE_EDIT:
                break;

            case CollectState.DETAILSMODE_NEW:
                CollectController.this.setCollectableComponentModelsInDetailsPanelMultiEditable(false);
                Clct clctNew = CollectController.this.newCollectableWithDefaultValues();

                CollectController.this.safeFillDetailsPanel(clctNew);
                break;

            case CollectState.DETAILSMODE_NEW_SEARCHVALUE:
                CollectController.this.setCollectableComponentModelsInDetailsPanelMultiEditable(false);
                Clct clctNewSearchValues = CollectController.this.newCollectableWithDefaultValues();
                CollectController.this.safeFillDetailsPanel(clctNewSearchValues);

                //if(iOldModeOuterState == CollectState.OUTERSTATE_SEARCH && transferSearchPanelData()) {
                // transfer field data
                clctNewSearchValues = CollectController.this.newCollectableWithSearchValues(clctNewSearchValues);
                // transfer subform data
                CollectController.this.newCollectableWithDependantSearchValues();
                //}
                break;

            case CollectState.DETAILSMODE_NEW_CHANGED:
                break;

            case CollectState.DETAILSMODE_MULTIVIEW:
                CollectController.this.setCollectableComponentModelsInDetailsPanelMultiEditable(true);
                CollectController.this
                        .safeFillMultiEditDetailsPanel(CollectController.this.getCompleteSelectedCollectables());
                break;

            case CollectState.DETAILSMODE_MULTIEDIT:
                break;
            } // switch

            UIUtils.invokeOnDispatchThread(new Runnable() {
                @Override
                public void run() {
                    setToolbarButtonsForDetailsMode(iDetailsMode);
                    CollectController.this.getDetailsPanel().showToolbar(true);
                    performDetailsModeEntered();
                }
            });
        }

        /**
         * @deprecated Move to DetailsController and make protected again.
         */
        public void performDetailsModeEntered() {
            DetailsPanel dp = getDetailsPanel();
            dp.setStatusBarText(" ");
            ctlDetails.updateStatusBarIfNecessary();
            addChangeListenersForDetails();

            if (getCollectState().isDetailsModeMultiViewOrEdit()) {
                bIsLastTabDetailsModeMultiViewOrEdit = true;
            } else {
                bIsLastTabDetailsModeMultiViewOrEdit = false;
            }

            ctlDetails.displayCurrentRecordNumberInDetailsPanelStatusBar();

            if (getCollectState().isDetailsModeNew()) {
                getDetailsConroller().evaluateNewCollectable();
            }
        }

        public void setToolbarButtonsForDetailsMode(final int iDetailsMode) {
            Component focusedComp = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();

            final CollectPanel<Clct> pnlCollect = CollectController.this.getCollectPanel();
            final DetailsPanel pnlDetails = pnlCollect.getDetailsPanel();
            // enable/disable toolbar buttons:

            // "refresh/cancel" button:
            final boolean bDisguiseRefreshButton = CollectState.isDetailsModeNew(iDetailsMode)
                    || CollectState.isDetailsModeMultiViewOrEdit(iDetailsMode);
            pnlDetails.disguiseRefreshButton(bDisguiseRefreshButton);

            // "save" action:
            CollectController.this.getSaveAction().setEnabled(
                    CollectController.this.changesArePending() && CollectController.this.isSaveAllowed());

            // "refresh current collectable" action:
            final boolean bRefreshEnabled = ((iDetailsMode == CollectState.DETAILSMODE_VIEW)
                    || CollectState.isDetailsModeChangesPending(iDetailsMode))
                    && CollectController.this.isRefreshSelectedCollectableAllowed();
            CollectController.this.getRefreshCurrentCollectableAction().setEnabled(bRefreshEnabled);

            final boolean bViewingExistingRecord = (iDetailsMode == CollectState.DETAILSMODE_VIEW);

            // "delete" action:
            setDeleteActionEnabled(
                    bViewingExistingRecord && CollectController.this.isDeleteSelectedCollectableAllowed());

            // "clone" action:
            CollectController.this.getCloneAction()
                    .setEnabled(bViewingExistingRecord && CollectController.this.isCloneAllowed());

            // navigation actions:
            final boolean bNavigationEnabled = bViewingExistingRecord
                    && CollectController.this.isNavigationAllowed();

            final CollectNavigationModel collectNavigationModel = CollectController.this
                    .getCollectNavigationModel();
            assert !bNavigationEnabled || collectNavigationModel != null;

            CollectController.this.getFirstAction()
                    .setEnabled(bNavigationEnabled && !collectNavigationModel.isFirstElementSelected());
            CollectController.this.getLastAction()
                    .setEnabled(bNavigationEnabled && !collectNavigationModel.isLastElementSelected());
            CollectController.this.getPreviousAction()
                    .setEnabled(bNavigationEnabled && !collectNavigationModel.isFirstElementSelected());
            CollectController.this.getNextAction()
                    .setEnabled(bNavigationEnabled && !collectNavigationModel.isLastElementSelected());

            CollectController.this.getDetailsPanel().setStatusBarText(" ");
            CollectController.this.ctlDetails.updateStatusBarIfNecessary();

            CollectController.this.addChangeListenersForDetails();

            if (CollectController.this.getCollectState().isDetailsModeMultiViewOrEdit()) {
                CollectController.this.bIsLastTabDetailsModeMultiViewOrEdit = true;
            } else {
                CollectController.this.bIsLastTabDetailsModeMultiViewOrEdit = false;
            }

            CollectController.this.ctlDetails.displayCurrentRecordNumberInDetailsPanelStatusBar();

            if (focusedComp == null) {
                SwingUtilities.invokeLater(new Runnable() {

                    public void run() {
                        // @see NUCLOS-747
                        if (CollectController.this.getDetailsPanel() != null) {
                            CollectController.this.getDetailsPanel().requestFocusInWindow();
                        }
                    }
                });
            }
        }

        @Override
        public void detailsModeLeft(CollectStateEvent ev) {
            final int iOuterState = ev.getNewCollectState().getOuterState();

            switch (iOuterState) {
            case CollectState.OUTERSTATE_RESULT:
            case CollectState.OUTERSTATE_SEARCH:
            case CollectState.OUTERSTATE_UNDEFINED:
                broadcastCollectableEvent(getSelectedCollectable(), MessageType.CLCT_LEFT);
                break;
            }

            // ensure we don't have more than one listener for each component:
            CollectController.this.removeChangeListenersForDetails();

            // TODO all actions that are specific to the Details tab must be disabled here and initially
            UIUtils.invokeOnDispatchThread(new Runnable() {
                @Override
                public void run() {
                    CollectController.this.getSaveAction().setEnabled(false);
                    CollectController.this.getDetailsPanel().showToolbar(false);
                }
            });
        }

    } // inner class DefaultCollectStateListener

    protected String getLabelForStartTab() {
        String result = null;

        switch (CollectController.this.getCollectState().getOuterState()) {
        case CollectState.OUTERSTATE_DETAILS:
            if (this.getCollectState().isDetailsModeViewOrEdit()) {
                result = this.getSelectedCollectable().getIdentifierLabel();
            }
            break;
        case CollectState.OUTERSTATE_RESULT:
            if (this.getSelectedCollectables().size() == 1) {
                result = this.getSelectedCollectable().getIdentifierLabel();
            }
            break;
        }

        if (result == null) {
            return this.getEntityLabel() + " (" + this.getSelectedCollectableId() + ")";
        } else {
            return result.trim();
        }
    }

    public void enableToolbarButtonsForDetailsMode(final int iDetailsMode) {
        final CollectPanel<Clct> pnlCollect = this.getCollectPanel();
        if (pnlCollect == null) {
            // controller already closed
            return;
        }
        final DetailsPanel pnlDetails = pnlCollect.getDetailsPanel();
        // enable/disable toolbar buttons:

        // "refresh/cancel" button:
        final boolean bDisguiseRefreshButton = CollectState.isDetailsModeNew(iDetailsMode)
                || CollectState.isDetailsModeMultiViewOrEdit(iDetailsMode);
        pnlDetails.disguiseRefreshButton(bDisguiseRefreshButton);

        // "save" action:
        this.getSaveAction().setEnabled(this.changesArePending() && this.isSaveAllowed());

        // "refresh current collectable" action:
        final boolean bRefreshEnabled = ((iDetailsMode == CollectState.DETAILSMODE_VIEW)
                || CollectState.isDetailsModeChangesPending(iDetailsMode))
                && this.isRefreshSelectedCollectableAllowed();
        this.getRefreshCurrentCollectableAction().setEnabled(bRefreshEnabled);

        final boolean bViewingExistingRecord = (iDetailsMode == CollectState.DETAILSMODE_VIEW);

        // "delete" action:
        setDeleteActionEnabled(bViewingExistingRecord && this.isDeleteSelectedCollectableAllowed());

        // "clone" action:
        CollectController.this.getCloneAction()
                .setEnabled(bViewingExistingRecord && CollectController.this.isCloneAllowed());

        // navigation actions:
        final boolean bNavigationEnabled = bViewingExistingRecord && this.isNavigationAllowed();

        final CollectNavigationModel collectNavigationModel = this.getCollectNavigationModel();
        assert !bNavigationEnabled || collectNavigationModel != null;

        this.getFirstAction().setEnabled(bNavigationEnabled && !collectNavigationModel.isFirstElementSelected());
        this.getLastAction().setEnabled(bNavigationEnabled && !collectNavigationModel.isLastElementSelected());
        this.getPreviousAction().setEnabled(bNavigationEnabled && !collectNavigationModel.isFirstElementSelected());
        this.getNextAction().setEnabled(bNavigationEnabled && !collectNavigationModel.isLastElementSelected());
    }

    public void disableToolbarButtons() {
        this.getSaveAction().setEnabled(false);
        //this.getFirstAction().setEnabled(false);
        //this.getLastAction().setEnabled(false);
        //this.getPreviousAction().setEnabled(false);
        //this.getNextAction().setEnabled(false);
        this.getCloneAction().setEnabled(false);
        this.ctlDetails.getDeleteCurrentCollectableAction().setEnabled(false);
        this.getRefreshCurrentCollectableAction().setEnabled(false);
    }

    protected void setDeleteActionEnabled(boolean enabled) {
        this.ctlDetails.getDeleteCurrentCollectableAction().setEnabled(enabled);
    }

    private void acceptLookedUpCollectable(final ICollectableListOfValues clctlovSource) {
        try {
            Collectable first = null;
            List<Collectable> additionalCollectables = new ArrayList<Collectable>();
            for (Collectable clct : getSelectedCollectables()) {
                if (clct != null) {
                    if (first == null) {
                        first = clct;
                    } else {
                        additionalCollectables.add(clct);
                    }
                }
            }
            if (first != null) {
                clctlovSource.acceptLookedUpCollectable(first, additionalCollectables);
            }
        } catch (Exception ex) {
            Errors.getInstance().showExceptionDialog(getTab(), ex);
        }
    }

    @Override
    public void visitDragEnter(DropTargetDragEvent dtde) {
    }

    @Override
    public void visitDragExit(DropTargetEvent dte) {
    }

    @Override
    public void visitDragOver(DropTargetDragEvent dtde) {
        Point here = dtde.getLocation();
        openDetailsPanel(here);
    }

    @Override
    public void visitDrop(DropTargetDropEvent dtde) {
    }

    @Override
    public void visitDropActionChanged(DropTargetDragEvent dtde) {
    }

    private void openDetailsPanel(Point here) {
        int hereRow = getResultTable().rowAtPoint(here);

        CollectableTableModel<Collectable> model = (CollectableTableModel<Collectable>) getResultTable().getModel();

        final Collectable clctSelected = model.getCollectable(hereRow);
        try {
            if (clctSelected != null) {
                if (Modules.getInstance().isModuleEntity(getEntityName())) {
                    final CollectableGenericObject clctloSelected = (CollectableGenericObject) clctSelected;
                    // we must reload the partially loaded object:
                    final int iModuleId = clctloSelected.getGenericObjectCVO().getModuleId();
                    GenericObjectClientUtils.showDetails(iModuleId, clctloSelected.getId());
                } else {
                    final CollectableMasterDataWithDependants clctmdSelected = (CollectableMasterDataWithDependants) clctSelected;
                    getMainController().showDetails(clctmdSelected.getCollectableEntity().getName(),
                            clctmdSelected.getId());
                }
            }
        } catch (Exception e) {
            throw new NuclosFatalException(e);
        }
    }

    protected Map<String, Serializable> getContext() {
        return context;
    }

    protected void invoke(CommonRunnable runnable) throws CommonBusinessException {
        invokeWithInputRequiredSupport.invoke(runnable, getContext(), getTab());
    }

    public DetailsController<Clct> getDetailsConroller() {
        return ctlDetails;
    }

    public SearchFilter getMainFilter() {
        return mainFilter;
    }

    public void setMainFilter(SearchFilter mainFilter) {
        this.mainFilter = mainFilter;
        getResultPanel().getSearchFilterBar().setSelected(mainFilter, true);
    }

    public void setMainFilter(SearchFilter mainFilter, boolean notifyListener) {
        this.mainFilter = mainFilter;
        getResultPanel().getSearchFilterBar().setSelected(mainFilter, notifyListener);
    }

    private void _setMainFilterAndSearch(SearchFilter mainFilter) {
        this.mainFilter = mainFilter;
        getResultController().getSearchResultStrategy().cmdSearch();
    }

    private class MainFilterChangeListener implements ItemListener {

        public MainFilterChangeListener() {
            super();
        }

        @Override
        public void itemStateChanged(ItemEvent e) {
            if (e.getItem() != null && !(e.getItem() instanceof SearchFilter)) {
                throw new IllegalArgumentException(e.getItem().getClass().getName());
            }
            SearchFilter changedItem = (SearchFilter) e.getItem();
            if (changedItem == mainFilter) {
                //            cmdResetMainFilter();
            } else {
                _setMainFilterAndSearch((SearchFilter) e.getItem());
            }
            actResetMainFilter.setEnabled(mainFilter != null);
        }
    }

    private void cmdResetMainFilter() {
        getResultPanel().getSearchFilterBar().setSelected(null);
    }

    public abstract String getCustomUsage();

} // class CollectController