OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | 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
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 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 org.openconcerto.sql.model.FieldPath;
182 ilm 17
import org.openconcerto.sql.model.RowRef;
73 ilm 18
import org.openconcerto.sql.model.SQLField;
182 ilm 19
import org.openconcerto.sql.model.SQLRowAccessor;
17 ilm 20
import org.openconcerto.sql.model.SQLRowValues;
73 ilm 21
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
17 ilm 22
import org.openconcerto.sql.model.SQLTable;
142 ilm 23
import org.openconcerto.sql.model.SQLTable.VirtualFields;
17 ilm 24
import org.openconcerto.sql.model.graph.Path;
182 ilm 25
import org.openconcerto.sql.request.BaseFillSQLRequest.OrderValue;
26
import org.openconcerto.sql.request.ComboSQLRequest.KeepMode;
73 ilm 27
import org.openconcerto.sql.view.list.search.SearchQueue;
17 ilm 28
import org.openconcerto.utils.CollectionUtils;
151 ilm 29
import org.openconcerto.utils.NumberUtils;
17 ilm 30
 
31
import java.util.ArrayList;
32
import java.util.Collection;
33
import java.util.Collections;
34
import java.util.HashSet;
35
import java.util.List;
36
import java.util.Set;
37
 
93 ilm 38
import net.jcip.annotations.GuardedBy;
39
import net.jcip.annotations.ThreadSafe;
40
 
17 ilm 41
/**
42
 * A line used by SQLTableModelSource, posessing an order and an id. Compare is done on the order.
43
 *
44
 * @author Sylvain
45
 */
93 ilm 46
@ThreadSafe
17 ilm 47
public final class ListSQLLine implements Comparable<ListSQLLine> {
48
 
49
    public static final int indexFromID(final List<ListSQLLine> l, final int id) {
50
        int foundIndex = -1;
51
        final int size = l.size();
52
        for (int i = 0; i < size; i++) {
53
            final int currentID = l.get(i).getID();
54
            if (currentID == id) {
55
                foundIndex = i;
56
                break;
57
            }
58
        }
59
        return foundIndex;
60
    }
61
 
93 ilm 62
    static final ListSQLLine fromID(final List<ListSQLLine> list, final int id) {
63
        final int size = list.size();
64
        for (int i = 0; i < size; i++) {
65
            final ListSQLLine line = list.get(i);
66
            if (line.getID() == id)
67
                return line;
68
        }
69
        return null;
70
    }
71
 
17 ilm 72
    private final SQLTableModelLinesSource src;
93 ilm 73
    // unmodifiable
74
    @GuardedBy("this")
182 ilm 75
    private SQLRowAccessor row;
76
    // If the whole graph of this.row isn't kept then we need to store the primary keys so that when
77
    // a SQLTableEvent is fired, we can know which lines is affected.
78
    private final Set<RowRef> pks;
93 ilm 79
    // Immutable
142 ilm 80
    private final SQLTableModelSourceState state;
17 ilm 81
    private final int id;
93 ilm 82
    // allow to order by something not in the row
83
    @GuardedBy("this")
84
    private Number order;
182 ilm 85
    // If the whole graph of this.row isn't kept then we need to store rows needed to order the
86
    // lines. Otherwise don't allocate more memory just use this.rows.
87
    private final OrderValue reqOrderVal;
17 ilm 88
    // lists are accessed by Swing (model.getValueAt()) and
89
    // by the search queue (SearchRunnable#matchFilter(ListSQLLine line))
93 ilm 90
    // immutable
91
    @GuardedBy("this")
92
    private List<Object> list;
17 ilm 93
 
142 ilm 94
    ListSQLLine(SQLTableModelLinesSource src, SQLRowValues row, int id, final SQLTableModelSourceState state) {
17 ilm 95
        super();
96
        this.src = src;
93 ilm 97
        this.setRow(row);
17 ilm 98
        this.id = id;
142 ilm 99
        this.state = state;
182 ilm 100
        final boolean isGraph = src.getParent().getKeepMode() == KeepMode.GRAPH;
101
        this.reqOrderVal = isGraph ? null : state.getReq().createOrderValue(row);
102
        if (isGraph) {
103
            this.clearCache();
104
            this.pks = null;
105
        } else {
106
            // compute all cells,
107
            this.list = Collections.emptyList();
108
            this.loadCache(state.getAllColumns().getAllColumns().size());
109
            // then free memory by keeping a single SQLRow and only RowIDRef
110
            this.setRow(row.asRow());
111
            final Set<RowRef> tmpRefs = new HashSet<>(row.getGraphSize(), 1.0f);
112
            for (final SQLRowValues v : row.getGraph().getItems()) {
113
                tmpRefs.add(v.getRowRef());
114
            }
115
            this.pks = Collections.unmodifiableSet(tmpRefs);
116
        }
17 ilm 117
    }
118
 
119
    // load at least columnCount values
93 ilm 120
    // (to avoid loading debug columns, which took more time than the regular columns, ie more than
121
    // half the time was passed on almost never displayed values)
122
    private void loadCache(int columnCount) {
123
        updateList(columnCount, Collections.<Integer> emptySet());
17 ilm 124
    }
125
 
142 ilm 126
    public final SQLTableModelSourceState getState() {
127
        return this.state;
128
    }
129
 
130
    public final SQLTableModelColumns getColumns() {
131
        return this.getState().getAllColumns();
132
    }
133
 
17 ilm 134
    public final SQLTableModelLinesSource getSrc() {
135
        return this.src;
136
    }
137
 
182 ilm 138
    private final void setRow(SQLRowAccessor v) {
93 ilm 139
        if (!v.isFrozen())
140
            throw new IllegalArgumentException("Not frozen : " + v);
141
        synchronized (this) {
142
            this.row = v;
143
        }
144
    }
145
 
182 ilm 146
    public final SQLRowValues getRow() {
147
        return (SQLRowValues) this.getRowAccessor();
148
    }
149
 
150
    public synchronized final SQLRowAccessor getRowAccessor() {
17 ilm 151
        return this.row;
152
    }
153
 
182 ilm 154
    public final Set<RowRef> getPKs() {
155
        return this.pks;
156
    }
157
 
17 ilm 158
    @Override
159
    public int compareTo(ListSQLLine o) {
160
        if (this.src != o.src)
161
            throw new IllegalArgumentException(this.src + " != " + o.src);
162
        return this.src.compare(this, o);
163
    }
164
 
151 ilm 165
    static final int compareLikeRequest(final ListSQLLine l1, final ListSQLLine l2) {
166
        if (l1 == l2)
167
            return 0;
168
        if (l1.getState() != l2.getState())
169
            throw new IllegalArgumentException("Different states");
170
 
171
        final Number order1, order2;
172
        final SQLTableModelLinesSource src = l1.getSrc();
173
        // avoid comparing in the middle of a reordering
174
        synchronized (src.getModel().getUpdateQ().getFullList()) {
175
            order1 = l1.getOrder();
176
            order2 = l2.getOrder();
177
        }
178
        if (order1 != null) {
179
            if (order2 == null)
180
                throw new IllegalStateException("Order mismatch :\n" + order1 + " for " + l1 + " not coherent with\n" + order2 + " for " + l2);
181
            return NumberUtils.compare(order1, order2);
182
        } else {
183
            if (order2 != null)
184
                throw new IllegalStateException("Order mismatch :\n" + order1 + " for " + l1 + " not coherent with\n" + order2 + " for " + l2);
182 ilm 185
 
186
            final OrderValue orderVal1 = l1.getRequestOrderValue();
187
            if (orderVal1 != null) {
188
                final OrderValue orderVal2 = l2.getRequestOrderValue();
189
                return orderVal1.compareTo(orderVal2);
190
            } else {
191
                // assert because we already checked same state above
192
                assert l2.getRequestOrderValue() == null;
193
                return l1.getState().getReq().order(l1.getRow(), l2.getRow());
194
            }
151 ilm 195
        }
196
    }
197
 
17 ilm 198
    public int getID() {
199
        return this.id;
200
    }
201
 
93 ilm 202
    public synchronized final void setOrder(Number order) {
203
        this.order = order;
204
    }
205
 
206
    public synchronized final Number getOrder() {
207
        return this.order;
208
    }
209
 
182 ilm 210
    public final OrderValue getRequestOrderValue() {
211
        return this.reqOrderVal;
212
    }
213
 
17 ilm 214
    public synchronized List<Object> getList(int columnCount) {
215
        this.loadCache(columnCount);
93 ilm 216
        return this.list;
17 ilm 217
    }
218
 
93 ilm 219
    public Object getValueAt(int column) {
220
        return this.getList(column + 1).get(column);
221
    }
222
 
17 ilm 223
    public final void setValueAt(Object obj, int colIndex) {
142 ilm 224
        this.getColumns().getColumns().get(colIndex).put(this, obj);
17 ilm 225
    }
226
 
93 ilm 227
    // should update this.list at the passed indexes, and then recursively for dependent columns
228
    // return all updated indexes
229
    // for now dependent columns (i.e. getUsedCols()) aren't supported so just return our parameter
230
    private final Set<Integer> updateValueAt(final Set<Integer> colIndexes) {
80 ilm 231
        if (colIndexes.size() == 0)
93 ilm 232
            return colIndexes;
233
        updateList(-1, colIndexes);
234
        return colIndexes;
235
    }
236
 
237
    // @param newSize negative means use current value
238
    private final boolean updateList(int newSize, final Set<Integer> colsToUpdate) {
239
        final int minIndexToUpdate;
240
        if (colsToUpdate.isEmpty()) {
241
            minIndexToUpdate = -1;
242
        } else {
243
            minIndexToUpdate = Collections.min(colsToUpdate).intValue();
244
            if (minIndexToUpdate < 0)
245
                throw new IllegalArgumentException("Negative indexes : " + colsToUpdate);
246
        }
17 ilm 247
        synchronized (this) {
93 ilm 248
            final int alreadyLoaded = this.list.size();
249
            if (newSize < 0)
250
                newSize = alreadyLoaded;
251
            // if there's enough items and either nothing to update or the columns to update aren't
252
            // yet needed, return
253
            if (alreadyLoaded >= newSize && (minIndexToUpdate < 0 || minIndexToUpdate >= alreadyLoaded))
254
                return false;
255
            final List<Object> newList = new ArrayList<Object>(newSize);
256
            for (int i = 0; i < newSize; i++) {
257
                final Object o;
258
                if (i >= alreadyLoaded || colsToUpdate.contains(i))
142 ilm 259
                    o = this.getColumns().getAllColumns().get(i).show(this.getRow());
93 ilm 260
                else
261
                    o = this.list.get(i);
262
                newList.add(o);
17 ilm 263
            }
93 ilm 264
            this.list = Collections.unmodifiableList(newList);
17 ilm 265
        }
93 ilm 266
        return true;
17 ilm 267
    }
268
 
269
    public void clearCache() {
182 ilm 270
        if (this.getSrc().getParent().getKeepMode() != KeepMode.GRAPH)
271
            throw new IllegalStateException("Wouldn't be able to compute cell values");
17 ilm 272
        synchronized (this) {
93 ilm 273
            this.list = Collections.emptyList();
17 ilm 274
        }
275
    }
276
 
277
    /**
278
     * Load the passed values into this row at the passed path.
279
     *
73 ilm 280
     * @param id ID of vals, needed when vals is <code>null</code>.
17 ilm 281
     * @param vals values to load, eg CONTACT.NOM = "Dupont".
282
     * @param p where to load the values, eg "SITE.ID_CONTACT_CHEF".
93 ilm 283
     * @return the columns that were affected, <code>null</code> meaning all.
17 ilm 284
     */
93 ilm 285
    synchronized Set<Integer> loadAt(int id, SQLRowValues vals, Path p) {
142 ilm 286
        assert vals == null || vals.getID() == id;
73 ilm 287
        final String lastReferentField = SearchQueue.getLastReferentField(p);
142 ilm 288
        // null vals means id was deleted, the only way we care is if it was pointing to us
289
        // (otherwise the foreign key pointing to it would have changed first)
290
        assert vals != null || lastReferentField != null;
17 ilm 291
        // load() empties vals, so getFields() before
73 ilm 292
        final Set<Integer> indexes = lastReferentField == null ? this.pathToIndex(p, vals.getFields()) : null;
17 ilm 293
        // replace our values with the new ones
93 ilm 294
        final SQLRowValues copy = this.getRow().deepCopy();
73 ilm 295
        if (lastReferentField == null) {
93 ilm 296
            for (final SQLRowValues v : copy.followPath(p, CreateMode.CREATE_NONE, false)) {
142 ilm 297
                // check id, e.g. if p is BATIMENT <- LOCAL -> FAMILLE_LOCAL, then there's multiple
298
                // familles
299
                if (v.getID() == id)
300
                    v.load(vals.deepCopy(), null);
73 ilm 301
            }
302
        } else {
303
            // e.g. if p is SITE <- BATIMENT <- LOCAL, lastField is LOCAL.ID_BATIMENT
304
            // if p is SITE -> CLIENT <- SITE (i.e. siblings of a site), lastField is SITE.ID_CLIENT
305
            final SQLField lastField = p.getStep(-1).getSingleField();
306
            final Collection<SQLRowValues> previous;
307
            if (p.length() > 1 && p.getStep(-2).reverse().equals(p.getStep(-1)))
93 ilm 308
                previous = copy.followPath(p.minusLast(2), CreateMode.CREATE_NONE, false);
73 ilm 309
            else
310
                previous = null;
311
            // the rows that vals should point to, e.g. BATIMENT or CLIENT
93 ilm 312
            final Collection<SQLRowValues> targets = copy.followPath(p.minusLast(), CreateMode.CREATE_NONE, false);
73 ilm 313
            for (final SQLRowValues target : targets) {
314
                // remove existing referent with the updated ID
315
                SQLRowValues toRemove = null;
316
                for (final SQLRowValues toUpdate : target.getReferentRows(lastField)) {
317
                    // don't back track (in the example a given SITE will be at the primary location
318
                    // and a second time along its siblings)
319
                    if ((previous == null || !previous.contains(toUpdate)) && toUpdate.getID() == id) {
320
                        if (toRemove != null)
93 ilm 321
                            throw new IllegalStateException("Duplicate IDs " + id + " : " + System.identityHashCode(toRemove) + " and " + System.identityHashCode(toUpdate) + "\n" + copy.printGraph());
73 ilm 322
                        toRemove = toUpdate;
323
                    }
324
                }
325
                if (toRemove != null)
326
                    toRemove.remove(lastField.getName());
156 ilm 327
                // attach updated values, foreign ID is always present but can be null
328
                if (vals != null && target.getIDNumber().equals(vals.getForeignIDNumberValue(lastField.getName()).getValue()))
73 ilm 329
                    vals.deepCopy().put(lastField.getName(), target);
330
            }
331
        }
93 ilm 332
        copy.getGraph().freeze();
333
        this.setRow(copy);
17 ilm 334
        // update our cache
93 ilm 335
        if (indexes == null) {
17 ilm 336
            this.clearCache();
93 ilm 337
            return null;
338
        } else {
339
            return this.updateValueAt(indexes);
340
        }
17 ilm 341
    }
342
 
343
    /**
344
     * Find the columns that use the modifiedFields for their value.
345
     *
346
     * @param p the path to the modified fields, eg "CPI.ID_LOCAL".
347
     * @param modifiedFields the field modified, eg "DESIGNATION".
348
     * @return the index of columns using "CPI.ID_LOCAL.DESIGNATION", or null for every columns.
349
     */
350
    private Set<Integer> pathToIndex(final Path p, final Collection<String> modifiedFields) {
93 ilm 351
        // TODO check if the column paths start with any of the modified foreign keys
352
        // since it's quite expensive, add a cache in SQLTableModelSource
73 ilm 353
        if (containsFK(p.getLast(), modifiedFields)) {
354
            // e.g. CPI.ID_LOCAL, easier to just refresh the whole line, than to search for each
355
            // column affected (that would mean expanding the FK)
17 ilm 356
            return null;
73 ilm 357
        } else {
17 ilm 358
            final Set<Integer> res = new HashSet<Integer>();
359
            final Set<FieldPath> modifiedPaths = FieldPath.create(p, modifiedFields);
142 ilm 360
            final List<? extends SQLTableModelColumn> cols = this.getColumns().getAllColumns();
17 ilm 361
            for (int i = 0; i < cols.size(); i++) {
362
                final SQLTableModelColumn col = cols.get(i);
363
                if (CollectionUtils.containsAny(col.getPaths(), modifiedPaths))
364
                    res.add(i);
365
            }
366
            return res;
367
        }
368
    }
369
 
370
    private static boolean containsFK(final SQLTable t, Collection<String> fields) {
142 ilm 371
        final Set<SQLField> ffs = t.getFields(VirtualFields.FOREIGN_KEYS);
17 ilm 372
        for (final String f : fields) {
142 ilm 373
            if (ffs.contains(t.getField(f)))
17 ilm 374
                return true;
375
        }
376
        return false;
377
    }
378
 
379
    @Override
380
    public String toString() {
182 ilm 381
        return this.getClass().getSimpleName() + " on " + this.getRowAccessor();
17 ilm 382
    }
383
}