OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 /*
 * Created on 5 nov. 2004
 */
package org.openconcerto.sql.element;

import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.Log;
import org.openconcerto.sql.TM;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLFieldsSet;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLSelect.LockStrength;
import org.openconcerto.sql.model.SQLType;
import org.openconcerto.sql.request.MutableRowItemView;
import org.openconcerto.sql.request.RowItemDesc;
import org.openconcerto.sql.request.RowNotFound;
import org.openconcerto.sql.request.SQLFieldTranslator;
import org.openconcerto.sql.request.SQLForeignRowItemView;
import org.openconcerto.sql.request.SQLRowItemView;
import org.openconcerto.sql.request.SQLRowView;
import org.openconcerto.sql.sqlobject.ElementComboBox;
import org.openconcerto.sql.sqlobject.SQLSearchableTextCombo;
import org.openconcerto.sql.sqlobject.SQLTextCombo;
import org.openconcerto.sql.sqlobject.itemview.SimpleRowItemView;
import org.openconcerto.sql.users.rights.UserRightsManager;
import org.openconcerto.ui.DisplayabilityListener;
import org.openconcerto.ui.FormLayouter;
import org.openconcerto.ui.JDate;
import org.openconcerto.ui.component.ComboLockedMode;
import org.openconcerto.ui.component.InteractionMode;
import org.openconcerto.ui.component.text.TextBehaviour;
import org.openconcerto.ui.component.text.TextComponentUtils;
import org.openconcerto.ui.coreanimation.Animator;
import org.openconcerto.ui.valuewrapper.BooleanValueWrapper;
import org.openconcerto.ui.valuewrapper.ValidatedValueWrapper;
import org.openconcerto.ui.valuewrapper.ValueWrapper;
import org.openconcerto.ui.valuewrapper.ValueWrapperFactory;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.DecimalUtils;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.cc.Transformer;
import org.openconcerto.utils.checks.EmptyListener;
import org.openconcerto.utils.checks.EmptyObj;
import org.openconcerto.utils.checks.ValidListener;
import org.openconcerto.utils.checks.ValidObject;
import org.openconcerto.utils.checks.ValidState;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Supplier;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import javax.swing.text.JTextComponent;

/**
 * Base class for SQLComponent. This implements the ValidObject by defining that a component is
 * valid when all its objects are valid and all its required objects are not empty : <br/>
 * <img src="doc-files/listeners.png"/>. Also this class uses SQLRowItemView to represent items of a
 * row.
 * 
 * @author ilm
 */
public abstract class BaseSQLComponent extends SQLComponent implements Scrollable, RIVPanel {
    protected static final String REQ = "required";
    protected static final String DEC = "notdecorated";
    protected static final String SEP = "noseparator";

    /**
     * Syntactic sugar for
     * {@link BaseSQLComponent#createSimpleRowItemView(String, Class, ITransformer)}.
     * 
     * @author Sylvain CUAZ
     * @param <T> type parameter
     */
    public static interface VWTransformer<T> extends ITransformer<ValueWrapper<? extends T>, ValueWrapper<? extends T>> {
    }

    private final SQLRowView requete;
    // whether the application (as opposed to us, the framework) allow the edition
    private final Map<String, Boolean> allowEditable;

    private final Set<SQLRowItemView> required;
    // contains the SQL name of required SQLRowItemView
    private final Set<String> requiredNames;

    // [ValidListener]
    private final List<ValidListener> listeners;

    private InteractionMode editable;
    private boolean alwaysEditable;
    private final Set<SQLField> hide;
    private FormLayouter additionalFieldsPanel;
    private boolean displayFieldName;

    public BaseSQLComponent(SQLElement element) {
        super(element);
        // Obligatoire pour L&F Nymbus
        this.setOpaque(true);

        // pouvoir prendre le focus
        // ATTN marche pas toujours : qd disabled, clic sur ITextArea ne remonte pas jusqu'à nous.
        this.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                BaseSQLComponent.this.requestFocusInWindow();
            }
        });
        this.required = new HashSet<SQLRowItemView>();
        this.requiredNames = this.createRequiredNames();
        this.listeners = new ArrayList<ValidListener>();
        this.hide = new HashSet<SQLField>();
        this.editable = InteractionMode.READ_WRITE;
        this.setNonExistantEditable(false);
        this.requete = new SQLRowView(this.getElement());
        // enable or disable our views and forward event
        this.requete.addListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                updateChildrenEditable();
                firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
            }
        }, SQLComponent.READ_ONLY_PROP);
        this.allowEditable = new HashMap<String, Boolean>();
        this.displayFieldName = false;
    }

    private final SQLRowView getRequest() {
        return this.requete;
    }

    protected final SQLField getField(String field) {
        return this.getTable().getField(field);
    }

    // *** add*

    public Component addView(String field) {
        return this.addView(field, null);
    }

    public Component addView(String field, String spec) {
        return this.addViewJComponent(field, spec);
    }

    private Component addViewJComponent(String field, Object spec) {
        if (getElement().getPrivateElement(field) != null) {
            // private
            final SQLComponent comp = this.getElement().getPrivateElement(field).createDefaultComponent();

            // TODO add a callback so that RowItemView is notified when added to a SQLComponent
            // avoiding the 'if instanceof ElementSQLObject' in addInitedView()
            final SpecParser parser = SpecParser.create(spec);
            DefaultElementSQLObject dobj = new DefaultElementSQLObject(this, comp);
            dobj.setDecorated(parser.isDecorated());
            dobj.showSeparator(parser.showSeparator());
            return this.addView((MutableRowItemView) dobj, field, parser);
        } else {
            return this.addView(createRowItemView(getComp(field, Object.class)), field, spec);
        }
    }

    @SuppressWarnings("unchecked")
    static private <T, U> ValueWrapper<? extends T> castVW(final ValueWrapper<? extends U> vw, final Class<U> vwType, final Class<T> wantedType) {
        if (wantedType.isAssignableFrom(vwType))
            return (ValueWrapper<? extends T>) vw;
        else
            throw new IllegalArgumentException("Value wrapper isn't of the wanted type");
    }

    private <T> ValueWrapper<? extends T> getComp(final String field, final Class<T> wantedType) {
        if (getElement().getPrivateElement(field) != null)
            // we create a MutableRowItemView and need SpecParser
            throw new IllegalArgumentException("Private fields not supported");

        final ValueWrapper<? extends T> comp;
        final SQLType type = getField(field).getType();
        if (getField(field).isKey()) {
            // foreign
            comp = createSubValueWrapper(new ElementComboBox(), type, wantedType);
        } else {
            if (Boolean.class.isAssignableFrom(type.getJavaType())) {
                // TODO hack to view the focus (should try to paint around the button)
                final JCheckBox cb = new JCheckBox(" ");
                cb.setOpaque(false);
                final JPanel panel = new JPanel(new BorderLayout()) {
                    @Override
                    public void setEnabled(boolean enabled) {
                        super.setEnabled(enabled);
                        cb.setEnabled(enabled);
                    }
                };
                panel.add(cb, BorderLayout.LINE_START);
                comp = addValidatedValueWrapper(castVW(new BooleanValueWrapper(panel, cb), Boolean.class, wantedType), type);
            } else if (Date.class.isAssignableFrom(type.getJavaType())) {
                comp = createSubValueWrapper(new JDate(), type, wantedType);
            } else if (String.class.isAssignableFrom(type.getJavaType()) && type.getSize() >= 512) {
                comp = createSubValueWrapper(new SQLSearchableTextCombo(ComboLockedMode.UNLOCKED, true), type, wantedType);
            } else {
                // regular
                comp = createSubValueWrapper(new SQLTextCombo(), type, wantedType);
            }
        }
        comp.getComp().setOpaque(false);
        return comp;
    }

    public final void addSQLObject(JComponent obj, String field) {
        this.addSQLObject(obj, field, null);
    }

    public Component addSQLObject(JComponent obj, String field, Object spec) {
        return this.addView(obj, field, spec);
    }

    public void addRequiredSQLObject(JComponent obj, String field, Object spec) {
        this.addSQLObject(obj, field, REQ + ";" + spec);
    }

    public void addRequiredSQLObject(JComponent obj, String field) {
        this.addSQLObject(obj, field, REQ);
    }

    public Component addView(JComponent comp, String fields) {
        return this.addView(comp, fields, null);
    }

    public Component addView(JComponent comp, String fields, Object specObj) {
        final MutableRowItemView rowItemView;
        if (comp instanceof MutableRowItemView) {
            rowItemView = (MutableRowItemView) comp;
        } else {
            final SQLField field = this.getField(SQLRow.toList(fields).get(0));
            rowItemView = createRowItemView(comp, field);
        }
        return this.addView(rowItemView, fields, specObj);
    }

    static public SimpleRowItemView<?> createRowItemView(JComponent comp, final SQLField field) {
        if (comp == null)
            throw new NullPointerException("comp for " + field + " is null");
        if (comp instanceof MutableRowItemView)
            throw new IllegalStateException("Comp is a MutableRowItemView, creating a SimpleRowItemView would ignore its methods : " + comp);
        return createRowItemView(createSubValueWrapper(comp, field.getType(), Object.class));
    }

    // just to make javac happy (type parameter for SimpleRowItemView)
    static private <T> SimpleRowItemView<T> createRowItemView(ValueWrapper<T> vw) {
        if (vw instanceof MutableRowItemView)
            throw new IllegalStateException("Comp is a MutableRowItemView, creating a SimpleRowItemView would ignore its methods : " + vw);
        return new SimpleRowItemView<T>(vw);
    }

    // useful when the exact type of field is known (e.g. String) and setValue() is needed
    static public final <T> ValueWrapper<T> createValueWrapper(JComponent comp, final SQLType type, final Class<T> wantedType) {
        // equality allow returned ValueWrapper to both produce and consume T
        if (!wantedType.equals(type.getJavaType()))
            throw new ClassCastException("wanted type " + wantedType + " is not " + type);
        final ValueWrapper<T> res = ValueWrapperFactory.create(comp, wantedType);
        return addValidatedValueWrapper(res, type);
    }

    // useful when the exact type of field isn't known (e.g. Number) and only getValue() is needed
    static public final <T> ValueWrapper<? extends T> createSubValueWrapper(JComponent comp, final SQLType type, final Class<T> wantedSuperType) {
        final Class<?> fieldClass = type.getJavaType();
        final ValueWrapper<? extends T> res = ValueWrapperFactory.create(comp, fieldClass.asSubclass(wantedSuperType));
        return addValidatedValueWrapper(res, type);
    }

    static private <T> ValueWrapper<T> addValidatedValueWrapper(ValueWrapper<T> res, final SQLType type) {
        final Class<?> fieldClass = type.getJavaType();
        if (String.class.isAssignableFrom(fieldClass)) {
            res = ValidatedValueWrapper.add(res, new ITransformer<T, ValidState>() {
                @Override
                public ValidState transformChecked(T t) {
                    final String s = (String) t;
                    final boolean ok = s == null || s.length() <= type.getSize();
                    // only compute string if needed
                    return ok ? ValidState.getTrueInstance() : ValidState.create(ok, TM.tr("sqlComp.stringValueTooLong", s.length() - type.getSize()));
                }
            });
            // other numeric SQL types are fixed size like their java counterparts
        } else if (BigDecimal.class.isAssignableFrom(fieldClass)) {
            final Integer decimalDigits = type.getDecimalDigits();
            final int intDigits = type.getSize() - decimalDigits;
            final String reason = TM.tr("sqlComp.bdTooHigh", intDigits, decimalDigits);
            res = ValidatedValueWrapper.add(res, new ITransformer<T, ValidState>() {
                @Override
                public ValidState transformChecked(T t) {
                    final BigDecimal bd = (BigDecimal) t;
                    // round first to get the correct number of integer digits, see
                    // http://www.postgresql.org/docs/8.4/interactive/datatype-numeric.html
                    return ValidState.create(bd == null || DecimalUtils.intDigits(DecimalUtils.round(bd, decimalDigits)) <= intDigits, reason);
                }
            });
        }
        return res;
    }

    public final <T> SimpleRowItemView<? extends T> createSimpleRowItemView(String fields, Class<T> clazz) {
        return this.createSimpleRowItemView(fields, clazz, Transformer.<ValueWrapper<? extends T>> nopTransformer());
    }

    /**
     * Create and initialize a SimpleRowItemView.
     * 
     * @param <T> type of field.
     * @param field field name.
     * @param clazz java type for the field.
     * @param init to initialize the value wrapper.
     * @return the created row item view.
     */
    public final <T> SimpleRowItemView<? extends T> createSimpleRowItemView(String field, Class<T> clazz, final ITransformer<? super ValueWrapper<? extends T>, ValueWrapper<? extends T>> init) {
        final ValueWrapper<? extends T> vw = this.getComp(field, clazz);
        if (vw instanceof MutableRowItemView)
            throw new IllegalStateException("Comp is a MutableRowItemView, creating a SimpleRowItemView would ignore its methods : " + vw);
        return initRIV(createRowItemView(init.transformChecked(vw)), field);
    }

    public Component addView(MutableRowItemView rowItemView, String fields, Object specObj) {
        return this.addInitedView(initRIV(rowItemView, fields), specObj);
    }

    private final <R extends MutableRowItemView> R initRIV(R rowItemView, String fields) {
        final List<String> fieldListS = SQLRow.toList(fields);
        final Set<SQLField> fieldList = new LinkedHashSet<SQLField>(fieldListS.size());
        for (final String fieldName : fieldListS) {
            fieldList.add(this.getField(fieldName));
        }

        // sqlName
        final String sqlName = fields;
        rowItemView.init(sqlName, fieldList);
        return rowItemView;
    }

    public Component addInitedView(SQLRowItemView v, Object specObj) {
        // if (obj == null)
        // throw new IllegalArgumentException("obj is null");

        final Spec spec = SpecParser.create(specObj);

        // ParentForeignField is always required
        final Set<String> reqNames = this.getRequiredNames();
        if (spec.isRequired() || v.getFields().contains(getElement().getParentForeignField()) || reqNames == null || reqNames.contains(v.getSQLName())) {
            this.required.add(v);
            if (v instanceof ElementSQLObject)
                ((ElementSQLObject) v).setRequired(true);
            if (reqNames != null)
                this.requiredNames.add(v.getSQLName());
        }
        this.getRequest().add(v);

        if (v instanceof SQLComponentItem) {
            ((SQLComponentItem) v).added(this, v);
        }
        // don't call the method twice
        if (v.getComp() != v && v.getComp() instanceof SQLComponentItem) {
            ((SQLComponentItem) v.getComp()).added(this, v);
        }
        // reset just before adding to the UI :
        // - avoid updating value while displayed
        // - but still wait the longest to be sure the RIV is initialized (SQLComponentItem)
        v.resetValue();

        if (!CollectionUtils.containsAny(this.hide, v.getFields())) {
            if (spec.isAdditional()) {
                if (this.additionalFieldsPanel == null)
                    Log.get().warning("No additionalFieldsPanel for " + v.getFields() + " : " + v);
                else
                    this.additionalFieldsPanel.add(getDesc(v), v.getComp());
            } else {
                this.addToUI(v, spec.getWhere());
            }
        }

        final JTextComponent textComp = TextComponentUtils.getTextComp(v.getComp());
        if (textComp != null)
            TextBehaviour.manage(textComp);

        return v.getComp();
    }

    private boolean dontEdit(SQLRowItemView v) {
        final Set<String> fieldsNames = new SQLFieldsSet(v.getFields()).getFieldsNames(getTable());
        return this.getMode() == Mode.READ_ONLY || CollectionUtils.containsAny(this.getElement().getReadOnlyFields(), fieldsNames)
                || (this.getMode() != Mode.INSERTION && CollectionUtils.containsAny(this.getElement().getInsertOnlyFields(), fieldsNames));
    }

    @Override
    protected final void inited() {
        super.inited();
        if (!(this instanceof GroupSQLComponent)) {
            for (final Entry<String, Supplier<? extends JComponent>> e : this.getElement().getAdditionalFields().entrySet()) {
                final SpecParser spec;
                // FIXME Required à spécifier directement depuis le module
                if (this.requiredNames != null && this.requiredNames.contains(e.getKey())) {
                    spec = new SpecParser(REQ, true);
                } else {
                    spec = new SpecParser(null, true);
                }
                final Supplier<? extends JComponent> comp = e.getValue();
                if (comp == null) {
                    // infer component
                    this.addViewJComponent(e.getKey(), spec);
                } else {
                    // create component
                    this.addView(comp.get(), e.getKey(), spec);
                }
            }
        }
        // assure that added views are consistent with our editable status
        this.updateChildrenEditable();
        for (final SQLRowItemView v : this.getRequest().getViews()) {
            v.addEmptyListener(new EmptyListener() {
                public void emptyChange(EmptyObj src, boolean newValue) {
                    emptyOrValidChanged((SQLRowItemView) src);
                }
            });
            v.addValidListener(new ValidListener() {
                public void validChange(ValidObject src, ValidState newValue) {
                    emptyOrValidChanged((SQLRowItemView) src);
                }
            });
            // initial status
            updateAnimate(v);
        }
        this.addHierarchyListener(new DisplayabilityListener() {
            @Override
            protected void displayabilityChanged(Component c) {
                getRequest().activate(c.isDisplayable());
            }
        });
        getRequest().activate(this.isDisplayable());
        this.fireValidChange();
        this.initDone();
    }

    protected void initDone() {

    }

    private void updateAnimate(final SQLRowItemView v) {
        if (v.getComp() != null)
            Animator.getInstance().animate(v.getComp(), !isItemViewValid(v));
    }

    protected void emptyOrValidChanged(SQLRowItemView v) {
        this.fireValidChange();
        updateAnimate(v);
    }

    protected void addToUI(SQLRowItemView v, String where) {
        // implement it to do nothing since subclass may choose not to use it
    }

    protected final void setAdditionalFieldsPanel(FormLayouter panel) {
        this.additionalFieldsPanel = panel;
    }

    public final void setViewsOrder(final Collection<String> names) {
        this.getRequest().setViewsOrder(names);
    }

    public final SQLRowItemView getView(String name) {
        return this.getRequest().getView(name);
    }

    protected final SQLRowItemView getView(Component comp) {
        return this.getRequest().getView(comp);
    }

    public final Map<String, SQLRowItemView> getViews() {
        return this.getRequest().getViewsMap();
    }

    /**
     * Return each view that has at least one of {@link SQLRowItemView#getFields() its fields} in
     * the passed set.
     * 
     * @param fields the fields to search for.
     * @return all views that contains <code>fields</code>.
     */
    public final List<SQLRowItemView> getViews(final Set<SQLField> fields) {
        final List<SQLRowItemView> res = new ArrayList<SQLRowItemView>();
        for (final SQLRowItemView v : this.getRequest().getViews()) {
            if (!Collections.disjoint(fields, v.getFields())) {
                res.add(v);
            }
        }
        return res;
    }

    protected final SQLForeignRowItemView getForeignView(SQLRowItemView v) {
        if (v instanceof SQLForeignRowItemView)
            return (SQLForeignRowItemView) v;
        else if (v.getComp() instanceof SQLForeignRowItemView)
            return (SQLForeignRowItemView) v.getComp();
        else
            throw new IllegalArgumentException("no SQLForeignRowItemView found for " + v);
    }

    public void addValidListener(ValidListener l) {
        this.listeners.add(l);
    }

    @Override
    public void removeValidListener(ValidListener l) {
        this.listeners.remove(l);
    }

    protected synchronized final void fireValidChange() {
        // ATTN called very often during a select() (for each SQLObject empty & value change)
        final ValidState validated = this.getValidState();
        for (final ValidListener l : this.listeners) {
            l.validChange(this, validated);
        }
    }

    private boolean isItemViewValid(final SQLRowItemView v) {
        return v.getValidState().isValid() && !(this.getRequired().contains(v) && v.isEmpty());
    }

    @Override
    public synchronized ValidState getValidState() {
        boolean res = true;
        final List<String> pbs = new ArrayList<String>();
        // tous nos objets sont valides ?
        for (final SQLRowItemView obj : this.getRequest().getViews()) {
            final ValidState state = obj.getValidState();
            if (!state.isValid()) {
                final String txt = state.getValidationText();
                final String explanation = TM.tr("sqlComp.invalidItem", "'" + getDesc(obj) + "'", txt != null ? 1 : 0, txt);
                pbs.add(explanation);
                res = false;
                // ne regarder si vide que pour les valides (souvent les non-valides sont vides car
                // il ne peuvent renvoyer de valeur)
            } else if (this.getRequired().contains(obj) && obj.isEmpty()) {
                pbs.add(TM.tr("sqlComp.emptyItem", "'" + getDesc(obj) + "'"));
                res = false;
            }
        }
        return ValidState.create(res, CollectionUtils.join(pbs, "\n"));
    }

    protected final String getDesc(final SQLRowItemView obj) {
        return getLabelFor(obj.getSQLName());
    }

    public final String getLabelFor(String itemName) {
        return getDesc(itemName, getRIVDesc(itemName)).get0();
    }

    // not public since desc could be different from getRIVDesc(itemName)
    protected final Tuple2<String, Boolean> getDesc(final String itemName, final RowItemDesc desc) {
        final boolean emptyLabel = desc.getLabel() == null || desc.getLabel().trim().length() == 0;
        final String l;
        if (emptyLabel) {
            l = itemName;
        } else {
            l = getLabel(itemName, desc) + (this.displayFieldName ? " [" + itemName + "]" : "");
        }
        return Tuple2.create(l, !emptyLabel);
    }

    protected final void toggleDisplayFieldsNames() {
        this.displayFieldName = !this.displayFieldName;
        this.updateUIAll();
    }

    protected String getLabel(final String itemName, final RowItemDesc desc) {
        return desc.getLabel();
    }

    /**
     * Allow the views of this component to be edited. For this to be editable other framework
     * predicates must be <code>true</code> (e.g. {@link #isSelectionReadOnly()}).
     * 
     * @param mode how the application allow the views to be edited.
     * @return <code>true</code> if the mode was changed.
     */
    @Override
    public final boolean setEditable(InteractionMode mode) {
        final boolean changed = mode != this.editable;
        if (changed) {
            this.editable = mode;
            this.interactionModeChanged(mode);
        }
        return changed;
    }

    protected void interactionModeChanged(InteractionMode mode) {
        this.updateChildrenEditable();
    }

    private final void updateChildrenEditable() {
        for (final SQLRowItemView o : this.getRequest().getViews()) {
            updateEditable(o);
        }
    }

    public final InteractionMode getEditable() {
        return this.editable;
    }

    public void allowEditable(final String name, final boolean b) {
        final SQLRowItemView view = this.getView(name);
        if (view == null)
            throw new IllegalArgumentException("No view named " + name);
        this.allowEditable(view, b);
    }

    /**
     * Allow the passed view to be edited. For the view to be editable other framework predicates
     * must allow it (e.g. {@link #getEditable()}).
     * 
     * @param view the view.
     * @param b <code>true</code> if the application allow the view to be edited.
     */
    public void allowEditable(final SQLRowItemView view, final boolean b) {
        if (view == null)
            throw new NullPointerException();
        this.allowEditable.put(view.getSQLName(), b);
        updateEditable(view);
    }

    @Override
    protected void modeChanged() {
        super.modeChanged();
        this.updateChildrenEditable();
    }

    private void updateEditable(final SQLRowItemView o) {
        // a view can only be editable if its parent is too and if the selected row is
        // if nonExistantEditable : always editable, otherwise id must exist
        // a view can be disabled for other arbitrary application (non framework) reasons
        final InteractionMode mode;
        if (this.getEditable() != InteractionMode.READ_WRITE) {
            mode = this.getEditable();
        } else {
            final boolean editable = !this.dontEdit(o) && !this.isSelectionReadOnly() && (this.isNonExistantEditable() || this.getSelectedID() != SQLRow.NONEXISTANT_ID)
                    && this.allowEditable.get(o.getSQLName()) != Boolean.FALSE;
            mode = editable ? InteractionMode.READ_WRITE : InteractionMode.READ_ONLY;
        }
        o.setEditable(mode);
    }

    // final : overload select() if need be
    public final void resetValue() {
        this.select(null);
    }

    public final void partialReset() {
        final Set<String> names = this.getPartialResetNames();
        if (names == null || names.size() > 0)
            this.select(null, names);
    }

    public final int insert() {
        return this.insert(null);
    }

    public int insert(SQLRow order) {
        try {
            if (!UserRightsManager.getCurrentUserRights().canAdd(getTable()))
                throw new SQLException("forbidden");
            if (order == null)
                return this.getRequest().insert().getID();
            else
                return this.getRequest().insert(order).getID();
        } catch (SQLException e) {
            ExceptionHandler.handle(this, TM.tr("sqlComp.insertError"), e);
            return -1;
        }
    }

    /**
     * Return the last known values of the {@link #getSelectedID() selection} in the DB.
     * 
     * @return the last known values.
     */
    @Override
    public final SQLRowValues getLastKnownDBVals() {
        return this.getRequest().getLastKnownDBVals();
    }

    @Override
    public final boolean updateLastKnownDBVals(final LockStrength ls) {
        final SQLRowValues current = this.getLastKnownDBVals();
        if (current == null)
            throw new IllegalStateException("No values to update");
        return this.getRequest().setLastKnownDBVals(current, this.getRequest().fetchRow(current.getID(), ls));
    }

    @Override
    public final void select(int id) {
        // TODO in an executor outside of the EDT
        this.select(this.getRequest().fetchRow(id, LockStrength.NONE));
    }

    @Override
    public void select(SQLRowAccessor r) {
        this.select(r, null);
    }

    public void select(SQLRowAccessor r, Set<String> views) {
        try {
            // allow null to pass, ease some code (eg new ListOfSomething().getTable() even if we
            // can't see the table)
            if (r != null && !UserRightsManager.getCurrentUserRights().canView(getTable()))
                throw new IllegalStateException("forbidden");
            // MAYBE prevent repaints of each SQLObject with
            // this.setIgnoreRepaint(true) or RepaintManager
            this.getRequest().select(r, views);
            this.updateChildrenEditable();

            // don't put defaults if non-editable (eg bottom part of ListeModifyPanel)
            if (r == null && this.isNonExistantEditable()) {
                // selectDefaults() after select() so that we have the final word
                // eg default DESIGNATION of OBS is "ok", but default DESIGNATION of TRANSFO.ID_OBS
                // is "good".
                this.selectDefaults(views);
            }
        } catch (RowNotFound e) {
            // l'id demandé n'existe pas : prévenir tout le monde
            this.getTable().fireRowDeleted(e.getRow().getID());
            ExceptionHandler.handle(this, TM.tr("sqlComp.deletedRow", e.getRow()), e);
        } catch (IllegalStateException e) {
            ExceptionHandler.handle(this, TM.tr("sqlComp.selectError", r), e);
        }
    }

    // private since it only changes views having default values : use #resetValue()
    private final void selectDefaults(Set<String> views) {
        final SQLRowValues defaults = this.createValidDefaults();
        if (defaults != null && defaults.getFields().size() > 0)
            this.getRequest().select(defaults, views);
    }

    public final void addFillingListener(final PropertyChangeListener l) {
        this.getRequest().addListener(l, "filling");
    }

    public final void rmFillingListener(final PropertyChangeListener l) {
        this.getRequest().rmListener(l, "filling");
    }

    public final void addSelectionListener(final PropertyChangeListener l) {
        this.getRequest().addListener(l, "selectedID");
    }

    public final void rmSelectionListener(final PropertyChangeListener l) {
        this.getRequest().rmListener(l, "selectedID");
    }

    @Override
    public void detach() {
        this.getRequest().detach();
    }

    /**
     * Are we filling our objects.
     * 
     * @return <code>true</code> if we're filling in values from the DB.
     */
    public final boolean isFilling() {
        return this.getRequest().isFilling();
    }

    public int getSelectedID() {
        assert (SwingUtilities.isEventDispatchThread());
        return this.getRequest().getSelectedID();
    }

    @Override
    public final boolean isSelectionReadOnly() {
        return this.getRequest().isReadOnlySelection();
    }

    public void update() {
        try {
            if (!UserRightsManager.getCurrentUserRights().canModify(getTable()))
                throw new SQLException("forbidden");
            this.getRequest().update();
        } catch (SQLException e) {
            ExceptionHandler.handle(this, TM.tr("sqlComp.updateError"), e);
        }
    }

    @Override
    public void archive() {
        try {
            if (!UserRightsManager.getCurrentUserRights().canDelete(getTable()))
                throw new SQLException("forbidden");
            if (this.isSelectionReadOnly())
                throw new SQLException("read only");
            // MAYBE for performance (avoid searching for references to cut):
            // if (this.isPrivate()) {
            // ((BaseSQLComponent) this.getSQLParent().getSQLParent()).getSelectedID();
            // this.getElement().archivePrivate(this.getSelectedID(), parentID);
            // } else
            this.getElement().archive(this.getSelectedID());
            this.resetValue();
        } catch (SQLException e) {
            ExceptionHandler.handle(this, TM.tr("sqlComp.archiveError", this), e);
        }
    }

    /**
     * The SQL names that are always reset after an insert. This implementation returns an empty set
     * : the component is only reset on {@link #getResetMode() visibility change}.
     * 
     * @return the names to reset, <code>null</code> meaning all.
     */
    public Set<String> getPartialResetNames() {
        return Collections.emptySet();
    }

    // ** required

    protected final Set<SQLRowItemView> getRequired() {
        return this.required;
    }

    /**
     * The SQL names that are required.
     * 
     * @return the required SQL names, <code>null</code> meaning all of them.
     */
    protected final Set<String> getRequiredNames() {
        return this.requiredNames == null ? null : Collections.unmodifiableSet(this.requiredNames);
    }

    /**
     * The sql names that are required. This implementation includes "DESIGNATION".
     * 
     * @return the required sql names, <code>null</code> meaning all of them.
     */
    protected Set<String> createRequiredNames() {
        return new HashSet<String>(Collections.singleton("DESIGNATION"));
    }

    public String toString() {
        return this.getClass() + " on " + this.getTable() + " " + this.getSelectedID();
    }

    public final boolean isNonExistantEditable() {
        return this.alwaysEditable;
    }

    public final void setNonExistantEditable(boolean alwaysEditable) {
        this.alwaysEditable = alwaysEditable;
    }

    public final RowItemDesc getRIVDesc(String field) {
        final Configuration conf = Configuration.getInstance();
        if (conf == null)
            return SQLFieldTranslator.NULL_DESC;
        else
            return conf.getTranslator().getDescFor(this.getTable(), getCode(), getElement().getMDPath(), field);
    }

    public final void setRIVDesc(String itemName, RowItemDesc desc) {
        try {
            Configuration.getTranslator(this.getTable()).storeDescFor(this.getTable(), getCode(), itemName, desc);
            updateUI(itemName, desc);
        } catch (SQLException e) {
            ExceptionHandler.handle(this, TM.tr("sqlComp.saveDocError", itemName), e);
        }
    }

    protected void updateUI(final String itemName, final RowItemDesc desc) {
    }

    protected final void updateUI(final String itemName, final JComponent label, final RowItemDesc desc) {
        updateUI(itemName, label, desc, null);
    }

    protected final void updateUI(final String itemName, final JComponent label, final RowItemDesc desc, final Color emptyLabelColor) {
        label.setToolTipText(desc.getDocumentation().trim().length() == 0 ? null : desc.getDocumentation());
        final Tuple2<String, Boolean> tuple = getDesc(itemName, desc);
        final String s = tuple.get0();
        if (label instanceof JLabel) {
            ((JLabel) label).setText(s);
        } else if (label instanceof JTextComponent) {
            ((JTextComponent) label).setText(s);
        } else if (label.getBorder() instanceof TitledBorder) {
            ((TitledBorder) label.getBorder()).setTitle(s);
        } else {
            Log.get().warning("Couldn't change label for " + itemName);
        }
        if (emptyLabelColor != null && !tuple.get1())
            label.setForeground(emptyLabelColor);
        label.repaint();
    }

    protected final void updateUIAll() {
        for (final SQLRowItemView v : this.getRequest().getViews()) {
            final String sqlName = v.getSQLName();
            updateUI(sqlName, getRIVDesc(sqlName));
        }
    }

    public void doNotShow(SQLField f) {
        this.hide.add(f);
    }

    private static interface Spec {

        boolean isRequired();

        String getWhere();

        boolean isAdditional();
    }

    private static final class SpecParser implements Spec {

        static public SpecParser create(Object specObj) {
            final SpecParser spec;
            if (specObj == null || specObj instanceof String) {
                spec = new SpecParser((String) specObj);
            } else {
                spec = (SpecParser) specObj;
            }
            return spec;
        }

        private boolean isRequired;
        private String where;
        private boolean showSeparator = true;
        private boolean isDecorated = true;
        private final boolean isAdditional;

        public SpecParser(String spec) {
            this(spec, false);
        }

        public SpecParser(String spec, final boolean isAdditional) {
            this.isAdditional = isAdditional;
            // empty string treated as null
            if (spec == null || spec.length() == 0) {
                this.isRequired = false;
                this.where = null;
            } else {
                final String[] specs = spec.split(";");
                if (specs.length > 4)
                    throw new IllegalArgumentException(spec);
                for (int i = 0; i < specs.length; i++) {
                    final String sp = specs[i];
                    if (sp.equals(REQ)) {
                        this.isRequired = true;
                    } else if (sp.equals(DEC)) {
                        this.isDecorated = false;
                    } else if (sp.equals(SEP)) {
                        this.showSeparator = false;
                    } else {
                        this.where = sp;
                    }
                }
            }
        }

        public boolean showSeparator() {
            return this.showSeparator;
        }

        public boolean isDecorated() {
            return this.isDecorated;
        }

        @Override
        public final boolean isRequired() {
            return this.isRequired;
        }

        @Override
        public final String getWhere() {
            return this.where;
        }

        @Override
        public final boolean isAdditional() {
            return this.isAdditional;
        }
    }

    @Override
    public SQLElementDirectory getDirectory() {
        return this.getElement().getDirectory();
    }

    @Override
    public SQLComponent getSQLComponent() {
        return this;
    }

    // *** scrollable

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        return this.getPreferredSize();
    }

    @Override
    public boolean getScrollableTracksViewportHeight() {
        if (getParent() instanceof JViewport) {
            // since getPreferredScrollableViewportSize() returns getPreferredSize() this component
            // will be shown by default at its preferred size, but if the viewport's height is
            // reduced this component will first shrink to its minimum size, and then below that
            // scrollbars will be displayed.
            return (((JViewport) getParent()).getHeight() >= getMinimumSize().height);
        }
        return false;
    }

    @Override
    public boolean getScrollableTracksViewportWidth() {
        // no horizontal scroll, too much pb with JTextArea when setLineWrap(true) : its minimum
        // width grows but never shrinks
        return true;
    }

    @Override
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        // difficult to tell because of the textAreas and the private components
        return 15;
    }

    @Override
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        final double length = orientation == SwingConstants.VERTICAL ? visibleRect.getHeight() : visibleRect.getWidth();
        // keep some common area
        return (int) (length - 30);
    }

}