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 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.model.ConnectionHandlerNoSetup;
import org.openconcerto.sql.model.OrderComparator;
import org.openconcerto.sql.model.SQLDataSource;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLTable.VirtualFields;
import org.openconcerto.sql.model.graph.Path;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.Value;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.ITransformerExn;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import net.jcip.annotations.NotThreadSafe;

/**
 * Lines are stored in SQLRowValues and committed to the database on demand. Unless otherwise noted,
 * public methods are thread-safe, others are required to be called from the {@link UpdateQueue}.
 * 
 * @author Sylvain
 */
public class SQLTableModelLinesSourceOffline extends SQLTableModelLinesSource {

    // row container with equals() using reference equality
    @NotThreadSafe
    static public final class Row {
        private final Integer id;
        private SQLRowValues vals;

        private Row(Integer id, SQLRowValues vals) {
            super();
            this.id = id;
            this.setRow(vals);
        }

        public final Integer getID() {
            return this.id;
        }

        public final SQLRowValues getRow() {
            return this.vals;
        }

        final void setRow(SQLRowValues newVals) {
            if (!newVals.isFrozen())
                throw new IllegalArgumentException("Not frozen : " + newVals);
            this.vals = newVals;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName() + " of " + this.getID();
        }
    }

    static private abstract class OfflineCallable<V> implements Callable<V> {

    }

    static private abstract class OfflineRunnable extends OfflineCallable<Object> implements Runnable {
        @Override
        public final Object call() throws Exception {
            this.run();
            return null;
        }
    }

    // all mutable attributes are accessed from the UpdateQueue thread

    private final SQLTableModelSourceOffline parent;
    private final List<Row> lines;
    // since new lines have no database ID, give them a virtual one
    private int freeID;
    private final Map<Integer, Row> id2line;
    // values that can be modified, read-only
    private final SQLRowValues modifiableVals;
    // original value for modified lines
    private final Map<Row, SQLRowValues> dbVals;
    // removed lines
    private final Set<Number> deleted;

    private boolean dbOrder;

    {
        this.lines = new LinkedList<Row>();
        // the firsts are used in other part of the fwk
        this.freeID = SQLRow.MIN_VALID_ID - 10;
        this.id2line = new HashMap<Integer, Row>();
        this.dbOrder = true;
    }

    public SQLTableModelLinesSourceOffline(SQLTableModelSourceOffline parent, ITableModel model) {
        super(model);
        this.parent = parent;
        this.modifiableVals = this.getParent().getElem().getPrivateGraph().toImmutable();
        this.dbVals = new HashMap<Row, SQLRowValues>();
        this.deleted = new HashSet<Number>();
    }

    @Override
    public final SQLTableModelSourceOffline getParent() {
        return this.parent;
    }

    private boolean isUpdateThread() {
        return this.getModel().getUpdateQ().currentlyInQueue();
    }

    private void checkUpdateThread() {
        if (!this.isUpdateThread())
            throw new IllegalStateException("Not in the update thread");
    }

    private final Row getRow(Integer id) {
        return this.getRow(id, false);
    }

    private final Row getRow(Integer id, final boolean required) {
        return this.getRow(id, required, null);
    }

    private final Row getRow(Integer id, final boolean required, final Object idSource) {
        final Row res = this.id2line.get(id);
        if (required && res == null)
            throw new IllegalArgumentException("Not in the list : " + (idSource == null ? id : idSource));
        return res;
    }

    public final Row getRowNow(Integer id, final boolean required) {
        checkUpdateThread();
        return this.getRow(id, required);
    }

    public final List<Row> getRowsNow() {
        checkUpdateThread();
        return Collections.unmodifiableList(this.lines);
    }

    protected final int getSize() {
        return this.lines.size();
    }

    private final int indexOf(Row r) {
        return this.lines.indexOf(r);
    }

    protected final List<ListSQLLine> getLines() {
        final List<ListSQLLine> res = new ArrayList<ListSQLLine>();
        for (final Row r : this.lines) {
            final ListSQLLine l = createLine(r);
            if (l != null)
                res.add(l);
        }
        return res;
    }

    // if the user has moved rows, DB order can no longer be used
    private final boolean isDBOrder() {
        return this.dbOrder;
    }

    private final boolean setDBOrder(final boolean dbOrder) {
        assert isUpdateThread();
        if (this.dbOrder != dbOrder) {
            this.dbOrder = dbOrder;
            return true;
        } else {
            return false;
        }
    }

    private final Number getOrder(final Row r) {
        if (this.isDBOrder())
            return null;
        else
            return this.indexOf(r);
    }

    private final ListSQLLine createLine(final Row r) {
        if (r == null)
            return null;
        final ListSQLLine res = this.createLine(r.vals, r.id);
        if (res != null)
            res.setOrder(getOrder(r));
        return res;
    }

    protected final List<SQLRowValues> fetch() {
        return this.getUpdateQueueReq().getValues();
    }

    /**
     * Fetch all rows and update our lines. Must be called by the {@link UpdateQueue}. Deleted rows
     * will be removed, inserted rows added, virtual rows unchanged, and updated rows will only be
     * updated if unchanged.
     * 
     * @return the new lines.
     */
    @Override
    public List<ListSQLLine> getAll() {
        assert isUpdateThread();
        final List<SQLRowValues> dbRows = this.fetch();

        if (this.lines.isEmpty()) {
            // optimization of the else block
            for (final SQLRowValues dbRow : dbRows) {
                this._add(dbRow, false, false);
            }
        } else {
            // delete
            final Set<Integer> dbIDs = new HashSet<Integer>();
            for (final SQLRowValues dbRow : dbRows) {
                dbIDs.add(dbRow.getIDNumber(true).intValue());
            }
            final Set<Integer> deletedIDs = new HashSet<Integer>(this.id2line.keySet());
            deletedIDs.removeAll(dbIDs);
            for (final Integer id : deletedIDs) {
                // don't delete virtual rows
                if (id.intValue() >= SQLRow.MIN_VALID_ID) {
                    final Value<ListSQLLine> val = this.updateRow(id.intValue(), null);
                    assert val.getValue() == null;
                }
            }
            // update/insert
            for (final SQLRowValues dbRow : dbRows) {
                this.updateRow(dbRow.getID(), dbRow);
            }
        }
        return this.getLines();
    }

    // row is null, if it was deleted
    private final Value<ListSQLLine> updateRow(final int id, final SQLRowValues row) {
        final Row existingLine = this.getRow(id);

        // if the row wasn't removed and we updated it, ignore new values
        if (row != null && existingLine != null && this.dbVals.containsKey(existingLine)) {
            // MAYBE warn if ignoring changes
            return Value.getNone();
        } else {
            final Row newRow;
            if (row == null) {
                // if this id is not part of us, rm from our list
                this._rm(existingLine);
                newRow = null;
            } else if (existingLine != null) {
                existingLine.setRow(row);
                newRow = existingLine;
            } else {
                // don't fire as the new line is returned from this method
                newRow = this._add(row, false, false);
            }
            return Value.getSome(this.createLine(newRow));
        }
    }

    /**
     * {@inheritDoc} Must be called by the {@link UpdateQueue}.
     */
    @Override
    public Value<ListSQLLine> get(final int id) {
        assert isUpdateThread();
        return updateRow(id, this.getUpdateQueueReq().getValues(id));
    }

    // *** Modify virtual rows ***

    private final <T> Future<T> execInUpdateQ(final OfflineCallable<T> call) {
        return this.getModel().getUpdateQ().execute(new FutureTask<T>(call));
    }

    public final Future<Number> add(final SQLRowValues vals) {
        // copy to avoid back-door and allow grow()
        final SQLRowValues copy = vals.deepCopy();
        return this.execInUpdateQ(new OfflineCallable<Number>() {
            @Override
            public Number call() throws Exception {
                return addNow(copy, true);
            }
        });
    }

    /**
     * Add a new row. This method can only be called from the update thread, e.g. from
     * {@link #useRow(Number, ITransformerExn)}.
     * 
     * @param vals the values, its {@link SQLRowValues#getID() ID} will be ignored.
     * @return the ID for the new row.
     * @see #add(SQLRowValues)
     */
    public final Integer addNow(final SQLRowValues vals) {
        return addNow(vals, false);
    }

    private final Integer addNow(final SQLRowValues vals, final boolean safe) {
        if (!safe)
            checkUpdateThread();
        final SQLRowValues copy = safe ? vals : vals.deepCopy();
        // otherwise could replace an existing row (use replaceRow() for that)
        // existing DB rows can only be added by fetch()
        copy.clearPrimaryKeys();
        return this._add(copy, true, true).getID();
    }

    protected Row _add(final SQLRowValues vals, final boolean grow, final boolean fireAdd) {
        assert isUpdateThread();
        // make sure every needed path is there
        if (grow)
            vals.grow(getUpdateQueueReq().getGraphToFetch(), false);
        // ATTN only works because vals was just fetched or just copied
        vals.getGraph().freeze();
        final boolean fromDB = vals.hasID() && vals.getID() >= SQLRow.MIN_VALID_ID;
        final List<Integer> order;
        final Row r;
        r = new Row(fromDB ? vals.getID() : this.freeID--, vals);
        this.id2line.put(r.getID(), r);
        this.lines.add(r);

        if (!fromDB && this.setDBOrder(false)) {
            order = this.getIDsOrder();
            assert order != null;
        } else {
            order = null;
        }
        if (order != null) {
            this.getModel().getUpdateQ().reorder(order);
            // put a setList() in searchQ
        }
        if (fireAdd) {
            // even if the row is filtered (i.e. line is null), don't remove as the filter might
            // change afterwards
            final ListSQLLine line = createLine(r);
            this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), line);
            // add the line in fullList
            // put a addList() in searchQ
        }
        return r;
    }

    public final Future<SQLRowValues> remove(final Number id) {
        return this.execInUpdateQ(new OfflineCallable<SQLRowValues>() {
            @Override
            public SQLRowValues call() throws Exception {
                final Row r = getRow(id.intValue());
                return rm(r);
            }
        });
    }

    public final SQLRowValues removeNow(final Row r) {
        this.checkUpdateThread();
        return rm(r);
    }

    private SQLRowValues rm(final Row r) {
        if (r != null) {
            this._rm(r);
            // add to a list of id to archive if it's in the DB
            if (r.vals.hasID())
                this.deleted.add(r.vals.getIDNumber());
            this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), null);
            return r.vals;
        } else {
            return null;
        }
    }

    private void _rm(final Row l) {
        assert isUpdateThread();
        if (l != null) {
            this.lines.remove(l);
            this.id2line.remove(l.id);
            this.dbVals.remove(l);
        }
    }

    @Override
    public void commit(final ListSQLLine l, final Path path, final SQLRowValues vals) {
        checkCanModif(path);
        if (!vals.isFrozen())
            throw new IllegalArgumentException("Not frozen");
        this.execInUpdateQ(new OfflineRunnable() {
            @Override
            public void run() {
                getModel().getUpdateQ().updateLine(l, path, vals.getID(), vals);
                recordOriginal(l);
            }
        });
    }

    public Future<?> replaceRow(final Number id, final SQLRowValues vals) {
        checkCanModif(Path.get(vals.getTable()));
        final SQLRowValues copy = vals.deepCopy();
        return this.updateRow(id, new IClosure<SQLRowValues>() {
            @Override
            public void executeChecked(SQLRowValues newVals) {
                final Set<String> contentFields = newVals.getTable().getFieldsNames(VirtualFields.CONTENT);
                newVals.clearReferents().removeAll(contentFields);
                newVals.load(copy, contentFields);
                if (copy.hasReferents()) {
                    for (final Entry<SQLField, Set<SQLRowValues>> e : new SetMap<SQLField, SQLRowValues>(copy.getReferentsMap()).entrySet()) {
                        for (final SQLRowValues ref : e.getValue()) {
                            ref.put(e.getKey().getName(), newVals);
                        }
                    }
                }
                assert copy.getGraphSize() == 1;
            }
        }, false);
    }

    public Future<?> updateRow(final Number id, final SQLRowValues vals) {
        checkCanModif(Path.get(vals.getTable()));
        if (vals.getGraphSize() > 1)
            throw new IllegalArgumentException("This method doesn't merge graphs");
        final SQLRowValues copy = vals.deepCopy();
        return this.updateRow(id, new IClosure<SQLRowValues>() {
            @Override
            public void executeChecked(SQLRowValues newVals) {
                final Set<String> contentFields = newVals.getTable().getFieldsNames(VirtualFields.CONTENT);
                newVals.load(copy, contentFields);
            }
        }, false);
    }

    public Future<SQLRowValues> updateRow(final Number id, final IClosure<SQLRowValues> valsClosure) {
        return this.updateRow(id, valsClosure, true);
    }

    private Future<SQLRowValues> updateRow(final Number id, final IClosure<SQLRowValues> valsClosure, final boolean mdCanChange) {
        return this.execInUpdateQ(new OfflineCallable<SQLRowValues>() {
            @Override
            public SQLRowValues call() throws Exception {
                return _updateRow(getRow(id.intValue(), true), valsClosure, mdCanChange).vals;
            }
        });
    }

    public final SQLRowValues updateRowNow(final Row r, final IClosure<SQLRowValues> valsClosure) {
        checkUpdateThread();
        return _updateRow(r, valsClosure, true).vals;
    }

    protected Row _updateRow(final Row r, final IClosure<SQLRowValues> valsClosure, final boolean mdCanChange) {
        assert isUpdateThread();
        final SQLRowValues newVals = r.getRow().deepCopy();
        valsClosure.executeChecked(newVals);
        // make sure PK and metadata don't change
        if (mdCanChange) {
            final Set<String> notContent = newVals.getTable().getFieldsNames(VirtualFields.CONTENT.complement());
            newVals.removeAll(notContent);
            newVals.putAll(r.getRow().getValues(notContent, false));
        }
        assert CompareUtils.equals(r.getRow().getIDNumber(), newVals.getIDNumber());
        // make sure every needed path is there
        newVals.grow(getUpdateQueueReq().getGraphToFetch(), false);
        newVals.getGraph().freeze();
        setRow(r, newVals);

        // call createLine() to apply filter
        this.getModel().getUpdateQ().replaceLine(r.getID().intValue(), createLine(r));
        return r;
    }

    private void checkCanModif(Path path) {
        if (this.modifiableVals.followPath(path) == null)
            throw new IllegalArgumentException("can only modify " + this.modifiableVals);
    }

    private void recordOriginal(ListSQLLine l) {
        setRow(getRow(l.getID(), true), l.getRow());
    }

    private void setRow(Row r, SQLRowValues newVals) {
        // if the existing row isn't in the DB, no need to update, the new values will be inserted
        if (r.getRow().hasID() && !this.dbVals.containsKey(r)) {
            // copy the initial state
            this.dbVals.put(r, r.getRow());
        }
        r.setRow(newVals);
    }

    public final <T> Future<T> useRow(final Number id, final ITransformerExn<SQLRowValues, T, ?> valsTransf) {
        return this.execInUpdateQ(new OfflineCallable<T>() {
            @Override
            public T call() throws Exception {
                final SQLRowValues vals = getRow(id.intValue(), true).vals;
                assert vals.isFrozen();
                return valsTransf.transformChecked(vals);
            }
        });
    }

    /**
     * Execute the passed transformer in the update queue, allow to call any method ending in "Now".
     * 
     * @param rowsTransf what to do.
     * @return a future with the value returned by <code>rowsTransf</code>.
     */
    public final <T> Future<T> useRows(final ITransformerExn<SQLTableModelLinesSourceOffline, T, ?> rowsTransf) {
        return this.execInUpdateQ(new OfflineCallable<T>() {
            @Override
            public T call() throws Exception {
                return rowsTransf.transformChecked(SQLTableModelLinesSourceOffline.this);
            }
        });
    }

    // *** Order ***

    @Override
    public Future<?> moveBy(final List<? extends SQLRowAccessor> list, final int inc) {
        if (inc == 0 || list.size() == 0)
            return null;

        // since SQLRowValues isn't thread-safe, use Concurrent Collection to safely pass it to
        // another thread
        final List<SQLRowAccessor> copy = new CopyOnWriteArrayList<SQLRowAccessor>(list);
        return this.execInUpdateQ(new OfflineRunnable() {
            @Override
            public void run() {
                _moveBy(copy, inc);
            }
        });
    }

    protected void _moveBy(final List<? extends SQLRowAccessor> list, final int inc) {
        assert isUpdateThread();
        final int count = this.lines.size();
        final boolean after = inc > 0;

        final List<Integer> order;
        // same algorithm as MoveQueue
        int outerIndex = -1;
        final List<Row> ourLines = new ArrayList<Row>(list.size());
        for (final SQLRowAccessor r : list) {
            final Row ourLine = this.getRow(r.getID(), true, r);
            final int index = this.indexOf(ourLine);
            ourLines.add(ourLine);
            if (outerIndex < 0 || after && index > outerIndex || !after && index < outerIndex) {
                outerIndex = index;
            }
        }
        assert outerIndex >= 0 && ourLines.size() == list.size();

        this.lines.removeAll(ourLines);
        assert this.lines.size() == count - ourLines.size();
        final int newIndex = after ? outerIndex + inc - list.size() + 1 : outerIndex + inc;
        this.lines.addAll(newIndex, ourLines);
        assert this.lines.size() == count : "Move has changed the number of rows from " + count + " to " + this.lines.size();
        this.setDBOrder(false);
        order = this.getIDsOrder();
        this.getModel().getUpdateQ().reorder(order);
    }

    private List<Integer> getIDsOrder() {
        final List<Integer> ids = new ArrayList<Integer>();
        for (final Row r : this.lines)
            ids.add(r.getID());
        return ids;
    }

    @Override
    public Future<?> moveTo(final List<? extends Number> ids, final int index) {
        return this.execInUpdateQ(new OfflineRunnable() {
            @Override
            public void run() {
                _moveTo(ids, index);
            }
        });
    }

    protected void _moveTo(final List<?> ids, final int index) {
        assert isUpdateThread();
        final int count = this.lines.size();

        final List<Integer> order;
        final List<Row> list = new ArrayList<Row>(ids.size());
        for (final Object o : ids) {
            final Integer id = o instanceof SQLRowAccessor ? ((SQLRowAccessor) o).getID() : ((Number) o).intValue();
            list.add(this.getRow(id, true, o));
        }
        if (index <= 0) {
            this.lines.removeAll(list);
            this.lines.addAll(0, list);
        } else if (index >= this.lines.size()) {
            this.lines.removeAll(list);
            this.lines.addAll(list);
        } else {
            Row destLine = null;
            int i = index;
            boolean contains = true;
            while (i < this.lines.size() && contains) {
                destLine = this.lines.get(i);
                contains = list.contains(destLine);
                if (contains)
                    i++;
            }
            if (contains) {
                this.lines.removeAll(list);
                this.lines.addAll(list);
            } else {
                this.lines.removeAll(list);
                final int newIndex = this.indexOf(destLine);
                this.lines.addAll(newIndex, list);
            }
        }
        assert this.lines.size() == count : "Move has changed the number of rows from " + count + " to " + this.lines.size();
        this.setDBOrder(false);
        order = this.getIDsOrder();
        this.getModel().getUpdateQ().reorder(order);
    }

    // *** Roll back or Commit ***

    public final Future<Boolean> hasModifications() {
        return this.execInUpdateQ(new OfflineCallable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return hasModificationsNow();
            }
        });
    }

    public final boolean hasModificationsNow() {
        checkUpdateThread();
        // ATTN this.dbOrder doesn't check just for order but also for added rows
        return !this.dbVals.isEmpty() || !this.deleted.isEmpty() || !this.dbOrder;
    }

    /**
     * Lose any changes and refetch from the database.
     * 
     * @return the future.
     */
    public final Future<?> reset() {
        return this.execInUpdateQ(new OfflineRunnable() {
            @Override
            public void run() {
                _reset();
            }
        });
    }

    protected void _reset() {
        assert isUpdateThread();

        this.lines.clear();
        this.id2line.clear();
        this.dbVals.clear();
        this.deleted.clear();
        this.setDBOrder(true);

        for (final SQLRowValues r : this.fetch())
            this._add(r, false, false);
        this.getModel().getUpdateQ().setFullList(getLines(), null);
    }

    /**
     * Make all changes applied to this persistent.
     * 
     * @return the future.
     */
    public final Future<?> commit() {
        return this.execInUpdateQ(new OfflineCallable<Object>() {
            @Override
            public Object call() throws Exception {
                _commit();
                return null;
            }
        });
    }

    public final void commitNow() throws SQLException {
        checkUpdateThread();
        this._commit();
    }

    protected final void _commit() throws SQLException {
        assert isUpdateThread();

        // don't listen to every commit and then to re-order, just updateAll() at the end
        this.getModel().getUpdateQ().rmTableListener();

        try {
            SQLUtils.executeAtomic(this.getParent().getPrimaryTable().getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
                @Override
                public Object handle(SQLDataSource ds) throws SQLException {
                    coreCommit();
                    return null;
                }
            });
        } finally {
            this.getModel().getUpdateQ().addTableListener();
        }
        this._reset();
    }

    protected void coreCommit() throws SQLException {
        // delete. Must be done first (e.g. there's a unique constraint and a deleted row conflicts
        // with a new row)
        getParent().getElem().archiveIDs(this.deleted);
        this.deleted.clear();

        // ordered rows
        final Map<Row, SQLRow> newRows = new LinkedHashMap<Row, SQLRow>();
        // insert, copy since we will remove some of the lines
        for (final Row l : this.lines) {
            final SQLRow newRow;
            if (!l.getRow().hasID()) {
                // only commit modified values, avoid updating each local, batiment, etc
                newRow = l.getRow().prune(this.modifiableVals).commit();
            } else {
                newRow = l.getRow().asRow();
            }
            // if the line is to be updated, this will get replaced below but it won't
            // change the ordering of the map
            newRows.put(l, newRow);
        }

        // update
        for (final Map.Entry<Row, SQLRowValues> e : this.dbVals.entrySet()) {
            final Row l = e.getKey();
            assert newRows.containsKey(l);
            newRows.put(l, this.getParent().getElem().update(e.getValue(), l.getRow()).exec());
        }
        this.dbVals.clear();

        if (getParent().getPrimaryTable().isOrdered()) {
            final List<SQLRow> wantedOrder = new ArrayList<SQLRow>(newRows.values());
            final List<SQLRow> dbOrder = new ArrayList<SQLRow>(newRows.values());
            Collections.sort(dbOrder, OrderComparator.INSTANCE);
            if (!wantedOrder.equals(dbOrder)) {
                MoveQueue.moveAtOnce(wantedOrder.subList(1, wantedOrder.size()), true, wantedOrder.get(0));
            }
        }
    }
}