OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 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.
 */
 
 package org.openconcerto.sql.view.list;

import org.openconcerto.sql.element.SQLElement;
import org.openconcerto.sql.model.FieldPath;
import org.openconcerto.sql.model.IFieldPath;
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.SQLRowValues.ForeignCopyMode;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.request.BaseFillSQLRequest;
import org.openconcerto.sql.request.ComboSQLRequest.KeepMode;
import org.openconcerto.sql.request.ListSQLRequest;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.change.ListChangeIndex;
import org.openconcerto.utils.change.ListChangeRecorder;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.SwingUtilities;

import net.jcip.annotations.GuardedBy;

/**
 * Define the columns and lines for ITableModel.
 * 
 * @author Sylvain
 */
public abstract class SQLTableModelSource {

    protected static final class DebugRow extends BaseSQLTableModelColumn {
        private final SQLTable t;

        protected DebugRow(final SQLTable t) {
            // don't put SQLRowAccessor: it's an interface and thus JTable.getDefaultRenderer()
            // returns null
            super("Fields", Object.class);
            this.t = t;
        }

        @Override
        protected Object show_(SQLRowAccessor r) {
            if (r instanceof SQLRow)
                return r;
            else
                return new SQLRowValues((SQLRowValues) r, ForeignCopyMode.COPY_ID_OR_RM);
        }

        @Override
        public Set<SQLField> getFields() {
            return this.t.getFields();
        }

        @Override
        public Set<FieldPath> getPaths() {
            return FieldPath.create(Path.get(this.t), this.t.getFieldsName());
        }
    }

    private final ListSQLRequest req;
    private final SQLElement elem;
    // only from EDT
    private SQLRowValues inited;
    // this.cols + debugCols, unmodifiable
    @GuardedBy("this")
    private SQLTableModelColumns allCols;
    // only from EDT
    private final ListChangeRecorder<SQLTableModelColumn> cols;
    // only from EDT
    private final List<SQLTableModelColumn> debugCols;
    // to notify of columns change, better than having one listener per line
    private final List<WeakReference<SQLTableModelLinesSource>> lines;

    private final PropertyChangeSupport supp;

    {
        this.supp = new PropertyChangeSupport(this);
        this.lines = new ArrayList<WeakReference<SQLTableModelLinesSource>>();
    }

    public SQLTableModelSource(final ListSQLRequest req, final SQLElement elem) {
        if (elem == null)
            throw new IllegalArgumentException("Missing element");
        this.req = req;
        this.elem = elem;
        if (!this.getPrimaryTable().equals(this.elem.getTable()))
            throw new IllegalArgumentException("not the same table: " + this.getPrimaryTable() + " != " + this.elem);
        this.setAllCols(SQLTableModelColumns.empty());
        this.cols = new ListChangeRecorder<SQLTableModelColumn>(new ArrayList<SQLTableModelColumn>());
        this.debugCols = new ArrayList<SQLTableModelColumn>();
        this.inited = req.getGraph();
    }

    public SQLElement getElem() {
        return this.elem;
    }

    protected abstract KeepMode getKeepMode();

    // lazy initialization since this method calls colsChanged() which subclasses overload and
    // they need their own attribute that aren't set yet since super() must be the first statement.
    public void init() {
        assert SwingUtilities.isEventDispatchThread();
        if (this.inited == null)
            return;

        final SQLRowValues graph = this.inited;

        final boolean moreDebugCols = this.getKeepMode() == KeepMode.GRAPH;
        graph.walkFields(new IClosure<FieldPath>() {
            @Override
            public void executeChecked(final FieldPath input) {
                final SQLField f = input.getField();
                if (f.getTable().getLocalContentFields().contains(f)) {
                    final SQLTableModelColumnPath col = new SQLTableModelColumnPath(input, null, getElem().getDirectory());
                    SQLTableModelSource.this.cols.add(col);
                } else if (moreDebugCols) {
                    SQLTableModelSource.this.debugCols.add(new SQLTableModelColumnPath(input.getPath(), f.getName(), f.toString()) {
                        // don't show the rowValues since it's very verbose (and all content fields
                        // are already displayed as normal columns) and unsortable
                        @Override
                        protected Object show_(SQLRowAccessor r) {
                            final Object res = super.show_(r);
                            return res instanceof SQLRowValues ? ((SQLRowValues) res).getID() : res;
                        }
                    });
                }
            }
        }, true);

        if (moreDebugCols)
            this.debugCols.add(new DebugRow(getPrimaryTable()));
        final SQLField orderField = getPrimaryTable().getOrderField();
        if (orderField != null)
            this.debugCols.add(new SQLTableModelColumnPath(Path.get(getPrimaryTable()), orderField.getName(), "Order"));
        this.debugCols.add(new SQLTableModelColumnPath(Path.get(getPrimaryTable()), getPrimaryTable().getKey().getName(), "PrimaryKey"));

        // at the end so that fireColsChanged() can use it
        this.inited = null;
        listenToCols();
        updateCols(null);
    }

    public SQLTableModelSource(SQLTableModelSource src) {
        this.req = src.req;
        this.elem = src.elem;
        this.setAllCols(src.getAllColumns());
        this.cols = new ListChangeRecorder<SQLTableModelColumn>(new ArrayList<SQLTableModelColumn>(src.cols));
        this.debugCols = new ArrayList<SQLTableModelColumn>(src.debugCols);
        this.inited = null;
        listenToCols();
    }

    private void listenToCols() {
        assert SwingUtilities.isEventDispatchThread();
        // keep allCols in sync with cols, and listen to any change
        this.cols.getRecipe().addListener(new IClosure<ListChangeIndex<SQLTableModelColumn>>() {
            @Override
            public void executeChecked(ListChangeIndex<SQLTableModelColumn> change) {
                updateCols(change);
            }
        });
    }

    protected final void updateCols(ListChangeIndex<SQLTableModelColumn> change) {
        assert SwingUtilities.isEventDispatchThread();
        // do not fire while initializing
        assert this.inited == null;

        if (change != null && change.getItemsAdded().isEmpty() && change.getItemsRemoved().isEmpty())
            return;
        final SQLTableModelSourceState beforeState = this.createState();
        this.setAllCols(new SQLTableModelColumns(this.cols, this.debugCols));
        colsChanged(change == null ? new ListChangeIndex.Add<SQLTableModelColumn>(0, this.getAllColumns().getAllColumns()) : change);
        final SQLTableModelSourceState afterState = this.createState();
        fireColsChanged(beforeState, afterState);
    }

    protected final SQLTableModelSourceState createState() {
        return new SQLTableModelSourceState(this.getAllColumns(), this.getReq());
    }

    private final void colsChanged(final ListChangeIndex<SQLTableModelColumn> change) {
        final AtomicBoolean biggerGraph = new AtomicBoolean(false);
        // add needed fields for each new column
        this.getReq().changeGraphToFetch(new IClosure<SQLRowValues>() {
            @Override
            public void executeChecked(SQLRowValues g) {
                for (final SQLTableModelColumn col : change.getItemsAdded()) {
                    // DebugRow should uses all *fetched* fields, but since it cannot know them
                    // getPaths() return all fields in the table. So don't fetch all fields just for
                    // this debug column
                    if (!(col instanceof DebugRow)) {
                        for (final IFieldPath p : col.getPaths()) {
                            if (BaseFillSQLRequest.addToFetch(g, p.getPath(), Collections.singleton(p.getFieldName())))
                                biggerGraph.set(true);
                        }
                    }
                }
            }
        });
        if (biggerGraph.get() && !allowBiggerGraph())
            throw new IllegalStateException("Bigger graph not allowed");
    }

    protected abstract boolean allowBiggerGraph();

    private void fireColsChanged(final SQLTableModelSourceState beforeState, final SQLTableModelSourceState afterState) {
        // let know each of our LinesSource that the columns have changed
        for (final SQLTableModelLinesSource line : getLines()) {
            line.colsChanged(beforeState, afterState);
        }
        // before notifying our regular listeners
        this.supp.firePropertyChange("cols", null, this.cols);
    }

    private final List<SQLTableModelLinesSource> getLines() {
        final List<SQLTableModelLinesSource> res = new ArrayList<SQLTableModelLinesSource>();
        int i = 0;
        while (i < this.lines.size()) {
            final WeakReference<SQLTableModelLinesSource> l = this.lines.get(i);
            final SQLTableModelLinesSource line = l.get();
            if (line == null)
                this.lines.remove(i);
            else {
                res.add(line);
                i++;
            }
        }
        return res;
    }

    protected final int getLinesCount() {
        return this.getLines().size();
    }

    public final SQLTableModelLinesSource createLinesSource(ITableModel model) {
        this.init();
        final SQLTableModelLinesSource res = this._createLinesSource(model);
        this.lines.add(new WeakReference<SQLTableModelLinesSource>(res));
        return res;
    }

    protected abstract SQLTableModelLinesSource _createLinesSource(ITableModel model);

    /**
     * The maximum graph of the lines returned by {@link #createLinesSource(ITableModel)}.
     * 
     * @return the maximum graph of our lines.
     */
    public final SQLRowValues getMaxGraph() {
        return this.getReq().getGraphToFetch();
    }

    // * columns

    /**
     * The normal columns.
     * 
     * @return the normal columns.
     */
    public final List<SQLTableModelColumn> getColumns() {
        this.init();
        return this.cols;
    }

    private synchronized void setAllCols(SQLTableModelColumns allCols) {
        this.allCols = allCols;
    }

    /**
     * The normal columns plus some debug columns. Usually primary and foreign keys.
     * 
     * @return the debub columns.
     */
    public synchronized final SQLTableModelColumns getAllColumns() {
        return this.allCols;
    }

    public final void addDebugColumn(final SQLTableModelColumn col) {
        this.init();
        this.debugCols.add(col);
        updateCols(new ListChangeIndex.Add<SQLTableModelColumn>(this.getAllColumns().size(), Collections.singleton(col)));
    }

    public final SQLTableModelColumn getColumn(int index) {
        return this.getAllColumns().getAllColumns().get(index);
    }

    /**
     * All the columns that depends on the passed field.
     * 
     * @param f the field.
     * @return all columns needing <code>f</code>.
     */
    public final List<SQLTableModelColumn> getColumns(SQLField f) {
        return this.getAllColumns().getColumns(f);
    }

    /**
     * The column depending solely on the passed field.
     * 
     * @param f the field.
     * @return the column needing only <code>f</code>.
     * @throws IllegalArgumentException if more than one column matches.
     */
    public final SQLTableModelColumn getColumn(SQLField f) {
        return this.getAllColumns().getColumn(f);
    }

    /**
     * The column depending solely on the passed path.
     * 
     * @param fp the field path.
     * @return the column needing only <code>fp</code>.
     * @throws IllegalArgumentException if more than one column matches.
     */
    public final SQLTableModelColumn getColumn(FieldPath fp) {
        return this.getAllColumns().getColumn(fp);
    }

    public final void addColumnListener(PropertyChangeListener l) {
        this.supp.addPropertyChangeListener("cols", l);
    }

    public final void rmColumnListener(PropertyChangeListener l) {
        this.supp.removePropertyChangeListener("cols", l);
    }

    // * SQLIdentifier

    public final ListSQLRequest getReq() {
        return this.req;
    }

    public final SQLTable getPrimaryTable() {
        return this.getReq().getPrimaryTable();
    }

    /**
     * All the displayed tables, i.e. tables of {@link #getLineFields()}.
     * 
     * @return the displayed tables.
     */
    public final Set<SQLTable> getTables() {
        return new SQLFieldsSet(this.getLineFields()).getTables();
    }

    /**
     * All fields that affects a line of this source. I.e. not just the displayed fields, but also
     * the foreign keys, including intermediate ones (e.g. if this displays [BATIMENT.DES, CPI.DES]
     * LOCAL.ID_BATIMENT matters).
     * 
     * @return the fields affecting this.
     */
    public final Set<SQLField> getLineFields() {
        final Set<SQLField> res = new HashSet<SQLField>();
        for (final SQLRowValues v : getMaxGraph().getGraph().getItems()) {
            for (final String f : v.getFields())
                res.add(v.getTable().getField(f));
            if (v.getTable().isArchivable())
                res.add(v.getTable().getArchiveField());
        }
        return res;
    }

}