OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.view.list;
15
 
16
import static org.openconcerto.sql.view.list.ITableModel.SleepState.AWAKE;
17
import static org.openconcerto.sql.view.list.ITableModel.SleepState.HIBERNATING;
18
import static org.openconcerto.sql.view.list.ITableModel.SleepState.SLEEPING;
142 ilm 19
 
17 ilm 20
import org.openconcerto.sql.Log;
80 ilm 21
import org.openconcerto.sql.element.SQLComponent;
73 ilm 22
import org.openconcerto.sql.model.SQLRowAccessor;
80 ilm 23
import org.openconcerto.sql.model.SQLRowValues;
17 ilm 24
import org.openconcerto.sql.model.SQLTable;
25
import org.openconcerto.sql.users.rights.TableAllRights;
26
import org.openconcerto.sql.users.rights.UserRights;
27
import org.openconcerto.sql.users.rights.UserRightsManager;
28
import org.openconcerto.sql.view.list.search.SearchQueue;
93 ilm 29
import org.openconcerto.sql.view.search.ColumnSearchSpec;
30
import org.openconcerto.sql.view.search.SearchList;
17 ilm 31
import org.openconcerto.sql.view.search.SearchSpec;
93 ilm 32
import org.openconcerto.utils.SleepingQueue;
33
import org.openconcerto.utils.SleepingQueue.LethalFutureTask;
34
import org.openconcerto.utils.SleepingQueue.RunningState;
17 ilm 35
import org.openconcerto.utils.TableSorter;
93 ilm 36
import org.openconcerto.utils.cc.IPredicate;
17 ilm 37
 
38
import java.beans.PropertyChangeEvent;
39
import java.beans.PropertyChangeListener;
40
import java.beans.PropertyChangeSupport;
156 ilm 41
import java.lang.Thread.UncaughtExceptionHandler;
17 ilm 42
import java.util.ArrayList;
43
import java.util.Collection;
44
import java.util.Collections;
80 ilm 45
import java.util.LinkedList;
17 ilm 46
import java.util.List;
47
import java.util.Timer;
48
import java.util.TimerTask;
83 ilm 49
import java.util.concurrent.Future;
177 ilm 50
import java.util.concurrent.RunnableFuture;
93 ilm 51
import java.util.concurrent.TimeUnit;
52
import java.util.concurrent.TimeoutException;
17 ilm 53
import java.util.concurrent.atomic.AtomicInteger;
41 ilm 54
import java.util.logging.Level;
17 ilm 55
 
56
import javax.swing.SwingUtilities;
80 ilm 57
import javax.swing.event.TableModelEvent;
17 ilm 58
import javax.swing.event.TableModelListener;
59
import javax.swing.table.AbstractTableModel;
60
import javax.swing.table.TableModel;
61
 
93 ilm 62
import net.jcip.annotations.GuardedBy;
63
 
17 ilm 64
/**
93 ilm 65
 * A model that takes its values from a {@link SQLTableModelSource}. The request lines can be
66
 * searched using {@link #search(SearchSpec)}. Like all Swing model, it ought too be manipulated in
67
 * the EDT except explicitly noted. ATTN as soon as nobody listens to an instance (using
68
 * addTableModelListener()) it dies and cannot be used again.
17 ilm 69
 *
70
 * @author Sylvain CUAZ
156 ilm 71
 * @see #start()
17 ilm 72
 */
73
public class ITableModel extends AbstractTableModel {
74
    public static enum SleepState {
75
        /**
76
         * The model processes events as they arrive.
77
         */
78
        AWAKE,
79
        /**
80
         * The events are queued to be executed when {@link #AWAKE}.
81
         */
82
        SLEEPING,
83
        /**
84
         * The model is sleeping plus its list is emptied to release memory.
85
         */
86
        HIBERNATING
87
    }
88
 
93 ilm 89
    static public interface DyingQueueExceptionHandler {
90
        public void handle(final LethalFutureTask<?> f, final Exception e);
91
    }
92
 
17 ilm 93
    private static Timer autoHibernateTimer = null;
73 ilm 94
    // not editable since default editors are potentially not safe (no validation)
93 ilm 95
    private static boolean defaultCellsEditable = false;
96
    private static boolean defaultOrderEditable = true;
17 ilm 97
 
98
    public static Timer getAutoHibernateTimer() {
99
        if (autoHibernateTimer == null)
100
            autoHibernateTimer = new Timer(ITableModel.class.getSimpleName() + " auto-hibernate timer", true);
101
        return autoHibernateTimer;
102
    }
103
 
93 ilm 104
    public static void setDefaultCellsEditable(boolean defaultEditable) {
105
        assert SwingUtilities.isEventDispatchThread();
106
        ITableModel.defaultCellsEditable = defaultEditable;
17 ilm 107
    }
108
 
151 ilm 109
    public static final boolean isDefaultCellsEditable() {
110
        return ITableModel.defaultCellsEditable;
111
    }
112
 
93 ilm 113
    public static void setDefaultOrderEditable(boolean defaultEditable) {
114
        assert SwingUtilities.isEventDispatchThread();
115
        ITableModel.defaultOrderEditable = defaultEditable;
116
    }
117
 
151 ilm 118
    public static final boolean isDefaultOrderEditable() {
119
        return ITableModel.defaultOrderEditable;
120
    }
121
 
17 ilm 122
    /**
123
     * Return the line of a JTable at the passed index, handling {@link TableSorter}.
124
     *
125
     * @param m the model of a JTable.
126
     * @param row an index in the JTable.
127
     * @return the line at <code>row</code>.
128
     */
129
    public static ListSQLLine getLine(final TableModel m, int row) {
130
        if (m instanceof ITableModel)
131
            return ((ITableModel) m).getRow(row);
132
        else if (m instanceof TableSorter) {
133
            final TableSorter sorter = (TableSorter) m;
134
            return getLine(sorter.getTableModel(), sorter.modelIndex(row));
135
        } else
136
            throw new IllegalArgumentException("neither ITableModel nor TableSorter : " + m);
137
    }
138
 
139
    // comment remplir la table
140
    private final SQLTableModelLinesSource linesSource;
93 ilm 141
    private SQLTableModelColumns columns;
17 ilm 142
    private final List<String> colNames;
143
    // la liste des lignes
144
    private final List<ListSQLLine> liste;
145
    // si on est en train de maj liste
146
    private boolean updating;
147
    private boolean filledOnce;
148
 
149
    private final PropertyChangeSupport supp;
80 ilm 150
    private List<TableModelListener> fullListeners;
17 ilm 151
 
152
    private final UpdateQueue updateQ;
153
    private boolean loading;
93 ilm 154
 
17 ilm 155
    // sleep state
93 ilm 156
    @GuardedBy("runSleep")
17 ilm 157
    private SleepState wantedState;
93 ilm 158
    @GuardedBy("runSleep")
17 ilm 159
    private SleepState actualState;
93 ilm 160
    @GuardedBy("runSleep")
21 ilm 161
    private int hibernateDelay;
93 ilm 162
    @GuardedBy("runSleep")
17 ilm 163
    private TimerTask autoHibernate;
164
    // number of runnables needing our queue to be awake
165
    private final AtomicInteger runSleep;
93 ilm 166
 
17 ilm 167
    private final SearchQueue searchQ;
168
    private boolean searching;
169
 
170
    // whether we should allow edition
93 ilm 171
    private boolean cellsEditable, orderEditable;
17 ilm 172
    private boolean debug;
173
 
156 ilm 174
    @GuardedBy("this")
175
    private UncaughtExceptionHandler uncaughtExnHandler = null;
93 ilm 176
    private DyingQueueExceptionHandler dyingQueueHandler = null;
177
 
17 ilm 178
    public ITableModel(SQLTableModelSource src) {
179
        this.supp = new PropertyChangeSupport(this);
80 ilm 180
        this.fullListeners = new LinkedList<TableModelListener>();
17 ilm 181
 
182
        this.liste = new ArrayList<ListSQLLine>(100);
183
        this.updating = false;
184
        this.filledOnce = false;
185
 
151 ilm 186
        this.setCellsEditable(isDefaultCellsEditable());
187
        this.setOrderEditable(isDefaultOrderEditable());
17 ilm 188
        this.debug = false;
189
 
190
        // don't use CopyUtils.copy() since this prevent the use of anonymous inner class
191
        this.linesSource = src.createLinesSource(this);
192
        this.colNames = new ArrayList<String>();
93 ilm 193
        setCols(src.getAllColumns());
17 ilm 194
 
195
        this.updateQ = new UpdateQueue(this);
196
        this.loading = false;
197
        this.updateQ.addPropertyChangeListener(new PropertyChangeListener() {
198
            @Override
199
            public void propertyChange(final PropertyChangeEvent evt) {
200
                if (evt.getPropertyName().equals("beingRun")) {
177 ilm 201
                    final boolean isLoading = UpdateQueue.isUpdate((RunnableFuture<?>) evt.getNewValue());
17 ilm 202
                    SwingUtilities.invokeLater(new Runnable() {
203
                        @Override
204
                        public void run() {
205
                            setLoading(isLoading);
206
                        }
207
                    });
208
                }
209
            }
210
        });
93 ilm 211
 
17 ilm 212
        this.runSleep = new AtomicInteger(0);
93 ilm 213
        synchronized (this.runSleep) {
214
            this.actualState = SleepState.AWAKE;
215
            this.wantedState = this.actualState;
216
            this.setHibernateDelay(30);
217
            this.autoHibernate = null;
218
        }
17 ilm 219
        this.searchQ = new SearchQueue(new ListAccess(this));
220
        this.searching = false;
221
        this.searchQ.addPropertyChangeListener(new PropertyChangeListener() {
222
            @Override
223
            public void propertyChange(PropertyChangeEvent evt) {
224
                if (evt.getPropertyName().equals("beingRun")) {
177 ilm 225
                    final boolean isSearching = SearchQueue.isSearch((RunnableFuture<?>) evt.getNewValue());
17 ilm 226
                    SwingUtilities.invokeLater(new Runnable() {
227
                        @Override
228
                        public void run() {
229
                            setSearching(isSearching);
230
                        }
231
                    });
232
                }
233
            }
234
        });
235
    }
236
 
237
    void print(String s) {
41 ilm 238
        print(s, Level.FINE);
17 ilm 239
    }
240
 
41 ilm 241
    void print(String s, Level l) {
242
        Log.get().log(l, this.getTable() + " " + this.hashCode() + " : " + s);
243
    }
244
 
17 ilm 245
    /**
246
     * The passed runnable will be run in the EDT after all current actions in the queue have
93 ilm 247
     * finished. This method is thread-safe.
17 ilm 248
     *
249
     * @param r the runnable to run in Swing.
250
     */
251
    public void invokeLater(final Runnable r) {
252
        if (r == null)
253
            return;
254
        this.runnableAdded();
255
        this.updateQ.put(new Runnable() {
256
            public void run() {
257
                try {
258
                    getSearchQueue().put(new Runnable() {
259
                        public void run() {
260
                            SwingUtilities.invokeLater(r);
261
                        }
262
                    });
263
                } finally {
264
                    runnableCompleted();
265
                }
266
            }
267
        });
268
    }
269
 
93 ilm 270
    public void putExternalUpdated(String externalID, IPredicate<ListSQLLine> affectedPredicate) {
271
        this.getUpdateQ().putExternalUpdated(externalID, affectedPredicate);
272
    }
273
 
17 ilm 274
    // *** refresh
275
 
93 ilm 276
    /**
277
     * Our update queue. This method is thread-safe.
278
     *
279
     * @return the update queue.
280
     */
17 ilm 281
    final UpdateQueue getUpdateQ() {
282
        return this.updateQ;
283
    }
284
 
285
    /**
93 ilm 286
     * Recharge toutes les lignes depuis la base. This method is thread-safe.
17 ilm 287
     */
288
    public void updateAll() {
289
        this.updateQ.putUpdateAll();
290
    }
291
 
73 ilm 292
    /**
293
     * If there's a where not on the primary table, the list doesn't know which lines to refresh and
93 ilm 294
     * it must reload all lines. This method is thread-safe.
73 ilm 295
     *
296
     * @param b <code>true</code> if the list shouldn't search for lines to refresh, but just reload
297
     *        all of them.
298
     */
17 ilm 299
    public final void setAlwaysUpdateAll(final boolean b) {
300
        this.getUpdateQ().setAlwaysUpdateAll(b);
301
    }
302
 
303
    // *** change list
304
    // none are synchronized since, they all are called from the EDT
305
 
93 ilm 306
    // liste is sorted, null meaning only order has changed
307
    void setList(List<ListSQLLine> liste, SQLTableModelColumns columns) {
17 ilm 308
        this.setUpdating(true);
93 ilm 309
        if (liste == null) {
310
            Collections.sort(this.liste);
311
        } else {
312
            this.liste.clear();
313
            this.liste.addAll(liste);
314
            this.filledOnce = true;
315
            print("liste filled : " + this.liste.size());
316
        }
317
        if (columns != null && this.setCols(columns)) {
318
            this.fireTableStructureChanged();
319
        } else {
320
            this.fireTableDataChanged();
321
        }
17 ilm 322
        this.setUpdating(false);
323
    }
324
 
325
    void addToList(ListSQLLine modifiedLine) {
326
        this.setUpdating(true);
327
 
328
        this.liste.add(modifiedLine);
329
        Collections.sort(this.liste);
330
        final int index = this.indexFromID(modifiedLine.getID());
331
        this.fireTableRowsInserted(index, index);
332
 
333
        this.setUpdating(false);
334
    }
335
 
336
    // modifiedLine match : it must be displayed
337
    void fullListChanged(ListSQLLine modifiedLine, final Collection<Integer> modifiedFields) {
338
        this.setUpdating(true);
339
 
340
        final int index = this.indexFromID(modifiedLine.getID());
341
        final boolean orderChanged;
342
        if (index >= 0) {
343
            this.liste.set(index, modifiedLine);
344
            final boolean afterPred;
345
            if (index > 0)
346
                afterPred = modifiedLine.compareTo(this.liste.get(index - 1)) > 0;
347
            else
348
                afterPred = true;
349
            final boolean beforeSucc;
350
            if (index < this.liste.size() - 1)
351
                beforeSucc = modifiedLine.compareTo(this.liste.get(index + 1)) < 0;
352
            else
353
                beforeSucc = true;
354
            orderChanged = !(afterPred && beforeSucc);
355
        } else {
356
            this.liste.add(modifiedLine);
357
            orderChanged = true;
358
        }
359
        if (orderChanged) {
360
            Collections.sort(this.liste);
361
            this.fireTableDataChanged();
362
        } else {
363
            if (modifiedFields == null)
364
                this.fireTableRowsUpdated(index, index);
365
            else
366
                for (final Integer i : modifiedFields) {
80 ilm 367
                    this.fireTableCellUpdated(index, i);
17 ilm 368
                }
369
        }
370
 
371
        this.setUpdating(false);
372
    }
373
 
374
    void removeFromList(int id) {
375
        this.setUpdating(true);
376
 
377
        final int index = this.indexFromID(id);
378
        // si la ligne n'existe pas, rien à faire
379
        if (index >= 0) {
380
            this.liste.remove(index);
381
            this.fireTableRowsDeleted(index, index);
382
        }
383
 
384
        this.setUpdating(false);
385
    }
386
 
80 ilm 387
    @Override
388
    public void fireTableChanged(TableModelEvent e) {
389
        // only fire for currently displaying cells
390
        if (e.getColumn() == TableModelEvent.ALL_COLUMNS || e.getColumn() < this.getColumnCount()) {
391
            super.fireTableChanged(e);
392
        } else {
393
            for (final TableModelListener l : this.fullListeners) {
394
                l.tableChanged(e);
395
            }
396
        }
397
    }
398
 
17 ilm 399
    // *** tableModel
400
 
401
    protected void updateColNames() {
402
        // getColumnNames() used to take more than 20% of SearchRunnable.matchFilter(), so cache it.
403
        this.colNames.clear();
404
        for (final SQLTableModelColumn col : getCols())
405
            this.colNames.add(this.isDebug() ? col.getName() + " " + col.getPaths().toString() : col.getName());
406
    }
407
 
408
    public List<String> getColumnNames() {
409
        return this.colNames;
410
    }
411
 
93 ilm 412
    private boolean setCols(SQLTableModelColumns cols) {
413
        if (!cols.equals(this.columns)) {
414
            this.columns = cols;
415
            this.updateColNames();
416
            return true;
417
        } else {
418
            return false;
419
        }
17 ilm 420
    }
421
 
93 ilm 422
    final List<? extends SQLTableModelColumn> getCols() {
423
        return this.isDebug() ? this.columns.getAllColumns() : this.columns.getColumns();
424
    }
425
 
17 ilm 426
    public int getRowCount() {
427
        return this.liste.size();
428
    }
429
 
430
    /**
431
     * The total number of lines fetched. Equals to {@link #getRowCount()} if there's no search.
93 ilm 432
     * This method is thread-safe.
17 ilm 433
     *
434
     * @return the total number of lines, or 0 if the first fill hasn't completed.
435
     */
436
    public int getTotalRowCount() {
93 ilm 437
        return this.getUpdateQ().getFullListSize();
17 ilm 438
    }
439
 
440
    // pas besoin de synch les méthode ne se servant que des colonnes, elles ne changent pas
441
 
442
    public int getColumnCount() {
443
        return this.getColumnNames().size();
444
    }
445
 
446
    public String getColumnName(int columnIndex) {
447
        // handle null names (as opposed to .toString())
448
        return String.valueOf(this.getColumnNames().get(columnIndex));
449
    }
450
 
451
    public Class<?> getColumnClass(int columnIndex) {
452
        return this.getReq().getColumn(columnIndex).getValueClass();
453
    }
454
 
80 ilm 455
    public final void setEditable(boolean b) {
93 ilm 456
        this.setCellsEditable(b);
457
        this.setOrderEditable(b);
17 ilm 458
    }
459
 
93 ilm 460
    public final void setCellsEditable(boolean b) {
151 ilm 461
        if (this.cellsEditable != b) {
462
            this.cellsEditable = b;
463
            this.supp.firePropertyChange("cellsEditable", !this.cellsEditable, this.cellsEditable);
464
        }
93 ilm 465
    }
466
 
467
    public final boolean areCellsEditable() {
468
        return this.cellsEditable;
469
    }
470
 
471
    public final void setOrderEditable(boolean b) {
151 ilm 472
        if (this.orderEditable != b) {
473
            this.orderEditable = b;
474
            this.supp.firePropertyChange("orderEditable", !this.orderEditable, this.orderEditable);
475
        }
93 ilm 476
    }
477
 
478
    public final boolean isOrderEditable() {
479
        return this.orderEditable;
480
    }
481
 
80 ilm 482
    @Override
17 ilm 483
    public boolean isCellEditable(int rowIndex, int columnIndex) {
93 ilm 484
        if (!this.areCellsEditable())
17 ilm 485
            return false;
486
        final SQLTableModelColumn col = this.getReq().getColumn(columnIndex);
487
        // hasRight is expensive so put it last
151 ilm 488
        return col.isEditable() && !isReadOnly(rowIndex, columnIndex, col) && hasRight(col);
17 ilm 489
    }
490
 
151 ilm 491
    private boolean isReadOnly(final int rowIndex, final int columnIndex, final SQLTableModelColumn col) {
492
        final ListSQLLine line = getRow(rowIndex);
493
        if (!line.getSrc().isCellEditable(line, columnIndex, col))
494
            return true;
495
        final SQLRowValues r = line.getRow();
80 ilm 496
        return r.getTable().contains(SQLComponent.READ_ONLY_FIELD) && SQLComponent.isReadOnly(r);
497
    }
498
 
17 ilm 499
    private boolean hasRight(final SQLTableModelColumn col) {
500
        final UserRights u = UserRightsManager.getCurrentUserRights();
83 ilm 501
        for (final SQLTable t : col.getWriteFields().getTables()) {
17 ilm 502
            if (!TableAllRights.hasRight(u, TableAllRights.MODIFY_ROW_TABLE, t))
503
                return false;
504
        }
83 ilm 505
        for (final SQLTable t : col.getWriteTables()) {
506
            if (!TableAllRights.hasRight(u, TableAllRights.ADD_ROW_TABLE, t) || !TableAllRights.hasRight(u, TableAllRights.DELETE_ROW_TABLE, t))
507
                return false;
508
        }
17 ilm 509
        return true;
510
    }
511
 
512
    public Object getValueAt(int rowIndex, int columnIndex) {
513
        if (rowIndex >= this.getRowCount())
514
            throw new IllegalArgumentException("!!!+ acces a la ligne :" + rowIndex + " et la taille est de:" + this.getRowCount());
93 ilm 515
        return getRow(rowIndex).getValueAt(columnIndex);
17 ilm 516
    }
517
 
518
    public final ListSQLLine getRow(int rowIndex) {
519
        return this.liste.get(rowIndex);
520
    }
521
 
522
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
523
        getRow(rowIndex).setValueAt(aValue, columnIndex);
524
    }
525
 
526
    // *** ids
527
 
528
    /**
529
     * Retourne l'ID de la ligne index.
530
     *
531
     * @param index la ligne dont on veut l'ID.
532
     * @return l'ID de la ligne voulue, ou -1 si index n'est pas valide.
533
     */
534
    public int idFromIndex(int index) {
535
        if (index >= 0 && this.liste.size() > index)
536
            return getRow(index).getID();
537
        else
538
            return -1;
539
    }
540
 
541
    /**
542
     * Retourne l'index de la ligne d'ID voulue.
543
     *
544
     * @param id l'id recherché.
545
     * @return l'index de la ligne correspondante, ou -1 si non trouvé.
546
     */
547
    public int indexFromID(int id) {
548
        return ListSQLLine.indexFromID(this.liste, id);
549
    }
550
 
93 ilm 551
    // *** search
552
 
17 ilm 553
    /**
93 ilm 554
     * Our search queue. This method is thread-safe.
17 ilm 555
     *
93 ilm 556
     * @return the search queue.
17 ilm 557
     */
558
    final SearchQueue getSearchQueue() {
559
        return this.searchQ;
560
    }
561
 
562
    /**
93 ilm 563
     * Effectue une recherche. This method is thread-safe.
17 ilm 564
     *
93 ilm 565
     * @param list description de la recherche à effectuer, can be <code>null</code>.
566
     * @param r sera exécuté dans la EDT une fois <code>list</code> recherchée.
17 ilm 567
     */
93 ilm 568
    public void search(final SearchSpec list, final Runnable r) {
569
        this.getSearchQueue().setSearch(list, r);
17 ilm 570
    }
571
 
93 ilm 572
    public void search(final SearchSpec list) {
17 ilm 573
        this.search(list, null);
574
    }
575
 
93 ilm 576
    public void resetSearch(final Runnable r) {
577
        this.search((SearchSpec) null, r);
578
    }
579
 
580
    public void resetSearch() {
581
        this.resetSearch(null);
582
    }
583
 
584
    // not public since colIndex can change before the list is searched
585
    // MAYBE pass column identifier
586
    void searchContains(final String s, final int colIndex, final Runnable r) {
587
        final SearchList spec = s == null ? null : SearchList.singleton(ColumnSearchSpec.create(s, colIndex));
588
        this.search(spec, r);
589
    }
590
 
591
    public void searchContains(final String s, final Runnable r) {
592
        this.searchContains(s, -1, r);
593
    }
594
 
17 ilm 595
    // *** move
596
 
93 ilm 597
    protected final boolean canOrder() {
151 ilm 598
        return this.isOrderEditable() && this.getReq().getReq().isTableOrder() && TableAllRights.hasRight(UserRightsManager.getCurrentUserRights(), TableAllRights.MODIFY_ROW_TABLE, getTable());
17 ilm 599
    }
600
 
93 ilm 601
    private final void handleCannotOrder(final boolean ignoreForbidden) {
602
        if (!ignoreForbidden)
603
            throw new IllegalStateException("Not allowed to order rows of " + getTable());
80 ilm 604
    }
605
 
93 ilm 606
    public void moveBy(final List<? extends SQLRowAccessor> rows, final int inc, final boolean ignoreForbidden) {
607
        if (canOrder())
608
            this.getLinesSource().moveBy(rows, inc);
609
        else
610
            handleCannotOrder(ignoreForbidden);
611
    }
612
 
613
    // take IDs :
614
    // 1. no need to protect rows from modifications, just copy IDs
615
    // 2. for non committed rows, i.e. without DB ID and thus without SQLRow, it's easier to handle
616
    // (virtual) IDs than to use IdentitySet of SQLRowValues
617
    public void moveTo(final List<Number> rows, final int rowIndex, final boolean ignoreForbidden) {
618
        if (canOrder())
619
            this.getLinesSource().moveTo(rows, rowIndex);
620
        else
621
            handleCannotOrder(ignoreForbidden);
622
    }
623
 
17 ilm 624
    /**
73 ilm 625
     * Search the row which is <code>inc</code> lines from rowID.
17 ilm 626
     *
627
     * @param rowID an ID of a row of this table.
628
     * @param inc the offset of visible lines.
73 ilm 629
     * @return the destination line or <code>null</code> if it's the same as <code>rowID</code> or
17 ilm 630
     *         <code>rowID</code> is inexistant.
631
     */
73 ilm 632
    ListSQLLine getDestLine(int rowID, int inc) {
17 ilm 633
        final int rowIndex = this.indexFromID(rowID);
634
        if (rowIndex < 0)
635
            return null;
636
        int destIndex = rowIndex + inc;
637
        final int min = 0;
638
        final int max = this.getRowCount() - 1;
639
        if (destIndex < min)
640
            destIndex = min;
641
        else if (destIndex > max)
642
            destIndex = max;
643
        if (destIndex != rowIndex) {
73 ilm 644
            return this.getRow(destIndex);
17 ilm 645
        } else
646
            return null;
647
    }
648
 
649
    // *** boolean
650
 
651
    /**
652
     * Whether this model has been filled at least once. Allow to differentiate between request has
653
     * not yet executed and request returned no rows.
654
     *
655
     * @return <code>true</code> if the rows reflect the database.
656
     */
657
    public final boolean filledOnce() {
658
        return this.filledOnce;
659
    }
660
 
93 ilm 661
    public final boolean isUpdating() {
17 ilm 662
        return this.updating;
663
    }
664
 
61 ilm 665
    // signify that the program is making a change not the user
666
    // i.e. should be called before and after every fireTable*()
93 ilm 667
    private void setUpdating(boolean searching) {
668
        assert SwingUtilities.isEventDispatchThread();
17 ilm 669
        final boolean old = this.updating;
670
        if (old != searching) {
671
            this.updating = searching;
672
            this.supp.firePropertyChange("updating", old, this.updating);
673
        }
674
    }
675
 
676
    private void setLoading(boolean isLoading) {
677
        // keep the value in an attribute since we are invoked later in EDT and by that time
678
        // the updateQueue might be doing something else and isLoading() could never return true
679
        final boolean old = this.loading;
680
        if (old != isLoading) {
681
            this.loading = isLoading;
682
            this.supp.firePropertyChange("loading", old, this.loading);
683
        }
684
    }
685
 
686
    public final boolean isLoading() {
687
        return this.loading;
688
    }
689
 
690
    private void setSearching(boolean searching) {
691
        final boolean old = this.searching;
692
        if (old != searching) {
693
            this.searching = searching;
694
            this.supp.firePropertyChange("searching", old, this.searching);
695
        }
696
    }
697
 
698
    public final boolean isSearching() {
699
        return this.searching;
700
    }
701
 
702
    // when the model is sleeping, no more updates are performed
703
    void setSleeping(boolean sleeping) {
704
        this.setSleeping(sleeping ? SleepState.SLEEPING : SleepState.AWAKE);
705
    }
706
 
707
    void setSleeping(SleepState state) {
41 ilm 708
        synchronized (this.runSleep) {
709
            this.wantedState = state;
710
            this.sleepUpdated();
17 ilm 711
        }
712
    }
713
 
21 ilm 714
    /**
715
     * Set the number of seconds between reaching the {@link SleepState#SLEEPING} state and setting
716
     * the {@link SleepState#HIBERNATING} state.
717
     *
718
     * @param seconds the number of seconds, less than 0 to disable automatic hibernating.
719
     */
720
    public final void setHibernateDelay(int seconds) {
93 ilm 721
        synchronized (this.runSleep) {
722
            this.hibernateDelay = seconds;
723
        }
21 ilm 724
    }
725
 
93 ilm 726
    public final int getHibernateDelay() {
727
        synchronized (this.runSleep) {
728
            return this.hibernateDelay;
729
        }
730
    }
731
 
17 ilm 732
    private void runnableAdded() {
733
        synchronized (this.runSleep) {
734
            this.runSleep.incrementAndGet();
735
            this.sleepUpdated();
736
        }
737
    }
738
 
739
    protected void runnableCompleted() {
740
        synchronized (this.runSleep) {
741
            this.runSleep.decrementAndGet();
742
            this.sleepUpdated();
743
        }
744
    }
745
 
746
    private void sleepUpdated() {
93 ilm 747
        assert Thread.holdsLock(this.runSleep);
17 ilm 748
        // set to null to do nothing
749
        final SleepState res;
750
        // if there's a user runnable we must wake up
751
        if (this.runSleep.get() > 0)
752
            res = AWAKE;
753
        // else we can go where we want
754
        else if (this.wantedState == this.actualState)
755
            res = null;
756
        else if (this.actualState == AWAKE) {
757
            // no need to test for runSleep
758
            // we cannot go from AWAKE directly to HIBERNATING
759
            res = SleepState.SLEEPING;
760
        } else if (this.actualState == SLEEPING) {
761
            res = this.wantedState;
762
        } else if (this.actualState == HIBERNATING) {
763
            // we cannot go from HIBERNATING to SLEEPING, since we are empty
764
            // besides we are already sleeping
765
            res = this.wantedState == AWAKE ? this.wantedState : null;
766
        } else
767
            throw new IllegalStateException("unknown state: " + this.actualState);
768
 
769
        if (res != null)
770
            this.setActual(res);
771
    }
772
 
773
    private void setActual(SleepState state) {
774
        if (this.actualState != state) {
775
            print("changing state " + this.actualState + " => " + state);
776
            this.actualState = state;
777
 
778
            if (this.autoHibernate != null)
779
                this.autoHibernate.cancel();
780
 
781
            switch (this.actualState) {
782
            case AWAKE:
783
                this.updateQ.setSleeping(false);
784
                break;
785
            case SLEEPING:
786
                this.updateQ.setSleeping(true);
93 ilm 787
                final int delay = this.getHibernateDelay();
788
                if (delay >= 0) {
21 ilm 789
                    this.autoHibernate = new TimerTask() {
790
                        @Override
791
                        public void run() {
41 ilm 792
                            try {
793
                                setSleeping(HIBERNATING);
794
                            } catch (Exception e) {
795
                                // never let an exception pass, otherwise the timer thread will die,
796
                                // and the *static* timer will become unusable for everyone
797
                                // OK to ignore setSleeping() since it's merely an optimization
798
                                print("HIBERNATING failed : " + e.getMessage(), Level.WARNING);
799
                                e.printStackTrace();
800
                            }
21 ilm 801
                        }
802
                    };
93 ilm 803
                    getAutoHibernateTimer().schedule(this.autoHibernate, delay * 1000);
21 ilm 804
                }
17 ilm 805
                break;
806
            case HIBERNATING:
807
                this.updateQ.putRemoveAll();
808
                break;
809
            }
810
 
811
            this.sleepUpdated();
812
        }
813
    }
814
 
815
    public final SQLTableModelLinesSource getLinesSource() {
816
        return this.linesSource;
817
    }
818
 
819
    public final SQLTableModelSource getReq() {
820
        return this.linesSource.getParent();
821
    }
822
 
823
    public final SQLTable getTable() {
824
        return this.getReq().getPrimaryTable();
825
    }
826
 
827
    public void addPropertyChangeListener(PropertyChangeListener l) {
828
        this.supp.addPropertyChangeListener(l);
829
    }
830
 
831
    public void addPropertyChangeListener(final String propName, PropertyChangeListener l) {
832
        this.supp.addPropertyChangeListener(propName, l);
833
    }
834
 
835
    public void rmPropertyChangeListener(PropertyChangeListener l) {
836
        this.supp.removePropertyChangeListener(l);
837
    }
838
 
839
    public void rmPropertyChangeListener(final String propName, PropertyChangeListener l) {
840
        this.supp.removePropertyChangeListener(propName, l);
841
    }
842
 
843
    public final boolean isDebug() {
844
        return this.debug;
845
    }
846
 
847
    /**
848
     * Set the debug mode : add keys to the normal columns and use the field names for the column
849
     * names.
850
     *
851
     * @param debug <code>true</code> to enable the debug mode.
852
     */
853
    public final void setDebug(boolean debug) {
93 ilm 854
        if (this.debug != debug) {
855
            this.setUpdating(true);
856
            this.debug = debug;
857
            this.updateColNames();
858
            this.fireTableStructureChanged();
859
            this.setUpdating(false);
860
        }
17 ilm 861
    }
862
 
863
    public String toString() {
864
        return this.getClass().getSimpleName() + "@" + this.hashCode() + " for " + this.getTable();
865
    }
866
 
80 ilm 867
    @Override
93 ilm 868
    public void addTableModelListener(TableModelListener l) {
80 ilm 869
        this.addTableModelListener(l, false);
870
    }
871
 
872
    /**
873
     * Adds a listener that's notified each time a change to the data model occurs.
874
     *
875
     * @param l the listener.
876
     * @param full if <code>true</code> <code>l</code> will be notified even if the data changed
877
     *        isn't displayed ({@link #setDebug(boolean) debug columns}).
878
     */
93 ilm 879
    public void addTableModelListener(TableModelListener l, final boolean full) {
880
        assert SwingUtilities.isEventDispatchThread();
881
        start();
80 ilm 882
        if (full)
883
            this.fullListeners.add(l);
17 ilm 884
        super.addTableModelListener(l);
885
    }
886
 
156 ilm 887
    public synchronized final void setUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExnHandler) {
888
        this.uncaughtExnHandler = uncaughtExnHandler;
889
    }
890
 
891
    public synchronized final UncaughtExceptionHandler getUncaughtExceptionHandler() {
892
        return this.uncaughtExnHandler;
893
    }
894
 
93 ilm 895
    public final void start() {
896
        final RunningState state = this.updateQ.getRunningState();
897
        if (state.compareTo(RunningState.RUNNING) > 0)
898
            throw new IllegalStateException("dead tableModel: " + this);
899
        if (state == RunningState.NEW) {
900
            print("starting");
151 ilm 901
            this.getLinesSource().live();
156 ilm 902
            this.startQueue(this.updateQ);
93 ilm 903
        }
904
    }
905
 
156 ilm 906
    final void startSearchQueue() {
907
        this.startQueue(this.getSearchQueue());
908
    }
909
 
910
    final void startQueue(final SleepingQueue q) {
911
        q.start((thr) -> {
912
            thr.setUncaughtExceptionHandler(getUncaughtExceptionHandler());
913
        });
914
    }
915
 
80 ilm 916
    @Override
93 ilm 917
    public void removeTableModelListener(TableModelListener l) {
918
        assert SwingUtilities.isEventDispatchThread();
80 ilm 919
        this.fullListeners.remove(l);
17 ilm 920
        super.removeTableModelListener(l);
921
        // nobody listens to us so we die
922
        if (this.listenerList.getListenerCount() == 0) {
93 ilm 923
            final RunningState threadState = this.updateQ.getRunningState();
924
            if (threadState == RunningState.RUNNING) {
925
                print("dying");
926
                // get() OK : updateQ and searchQ correctly killed
927
                final LethalFutureTask<?> dieSearch = this.updateQ.die();
928
                this.getLinesSource().die();
929
                synchronized (this.runSleep) {
930
                    if (this.autoHibernate != null) {
931
                        this.autoHibernate.cancel();
932
                        this.autoHibernate = null;
933
                    }
83 ilm 934
                }
93 ilm 935
                // Wait here so as to report problem with context. But pass a timeout so as not to
936
                // block the EDT.
937
                wait(dieSearch, 25, TimeUnit.MILLISECONDS);
938
            } else {
939
                print("not dying");
940
                Log.get().warning("Queue is " + threadState + " : unbalanced removeTableModelListener() called with " + l);
83 ilm 941
            }
17 ilm 942
        }
943
    }
944
 
93 ilm 945
    /**
946
     * What to do if a queue throws an exception while dying.
947
     *
948
     * @param h will be passed the exception thrown by {@link Future#get()} called on the result of
949
     *        {@link SleepingQueue#die()}, <code>null</code> to reset default behavior.
950
     */
951
    public void setDyingQueueExceptionHandler(final DyingQueueExceptionHandler h) {
156 ilm 952
        assert SwingUtilities.isEventDispatchThread();
93 ilm 953
        this.dyingQueueHandler = h;
17 ilm 954
    }
955
 
93 ilm 956
    public DyingQueueExceptionHandler getDyingQueueExceptionHandler() {
957
        return this.dyingQueueHandler;
958
    }
959
 
960
    final void wait(final LethalFutureTask<?> f, final long amount, final TimeUnit unit) {
961
        try {
962
            f.get(amount, unit);
963
            assert f.getQueue().getRunningState().compareTo(RunningState.DYING) >= 0;
964
        } catch (Exception e) {
965
            final DyingQueueExceptionHandler handler = getDyingQueueExceptionHandler();
966
            if (handler != null) {
967
                handler.handle(f, e);
968
            } else if (e instanceof TimeoutException) {
969
                Log.get().log(Level.INFO, "Killing still isn't done so create a watcher for " + f);
970
                SleepingQueue.watchDying(f, 15, 60, TimeUnit.SECONDS);
971
            } else {
972
                Log.get().log(Level.WARNING, "Exception while waiting, queue state is " + f.getQueue().getRunningState() + " for " + f, e);
973
            }
974
        }
975
    }
17 ilm 976
}