OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 177 | 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.request;
15
 
16
import org.openconcerto.sql.FieldExpander;
83 ilm 17
import org.openconcerto.sql.model.FieldRef;
142 ilm 18
import org.openconcerto.sql.model.IFieldPath;
151 ilm 19
import org.openconcerto.sql.model.OrderComparator;
17 ilm 20
import org.openconcerto.sql.model.SQLField;
182 ilm 21
import org.openconcerto.sql.model.SQLRow;
151 ilm 22
import org.openconcerto.sql.model.SQLRowAccessor;
17 ilm 23
import org.openconcerto.sql.model.SQLRowValues;
93 ilm 24
import org.openconcerto.sql.model.SQLRowValues.CreateMode;
17 ilm 25
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
83 ilm 26
import org.openconcerto.sql.model.SQLSearchMode;
17 ilm 27
import org.openconcerto.sql.model.SQLSelect;
142 ilm 28
import org.openconcerto.sql.model.SQLSyntax;
29
import org.openconcerto.sql.model.SQLSyntax.CaseBuilder;
30
import org.openconcerto.sql.model.SQLSyntax.DateProp;
17 ilm 31
import org.openconcerto.sql.model.SQLTable;
142 ilm 32
import org.openconcerto.sql.model.SQLTable.VirtualFields;
33
import org.openconcerto.sql.model.SQLTableModifiedListener;
34
import org.openconcerto.sql.model.SQLType;
17 ilm 35
import org.openconcerto.sql.model.Where;
36
import org.openconcerto.sql.model.graph.Path;
83 ilm 37
import org.openconcerto.utils.CollectionUtils;
142 ilm 38
import org.openconcerto.utils.Tuple2;
93 ilm 39
import org.openconcerto.utils.cc.IClosure;
17 ilm 40
import org.openconcerto.utils.cc.ITransformer;
41
 
42
import java.beans.PropertyChangeListener;
43
import java.beans.PropertyChangeSupport;
142 ilm 44
import java.sql.Date;
45
import java.sql.Time;
46
import java.sql.Timestamp;
47
import java.util.ArrayList;
83 ilm 48
import java.util.Arrays;
17 ilm 49
import java.util.Collection;
50
import java.util.Collections;
151 ilm 51
import java.util.Comparator;
83 ilm 52
import java.util.HashMap;
53
import java.util.HashSet;
17 ilm 54
import java.util.List;
142 ilm 55
import java.util.Locale;
83 ilm 56
import java.util.Map;
57
import java.util.Map.Entry;
58
import java.util.Set;
17 ilm 59
 
83 ilm 60
import net.jcip.annotations.GuardedBy;
93 ilm 61
import net.jcip.annotations.ThreadSafe;
83 ilm 62
 
93 ilm 63
@ThreadSafe
142 ilm 64
public abstract class BaseFillSQLRequest extends BaseSQLRequest {
17 ilm 65
 
73 ilm 66
    private static boolean DEFAULT_SELECT_LOCK = true;
67
 
17 ilm 68
    /**
69
     * Whether to use "FOR SHARE" in list requests (preventing roles with just SELECT right from
70
     * seeing the list).
93 ilm 71
     *
72
     * @return <code>true</code> if select should obtain a lock.
73
     * @see SQLSelect#setWaitPreviousWriteTX(boolean)
17 ilm 74
     */
73 ilm 75
    public static final boolean getDefaultLockSelect() {
76
        return DEFAULT_SELECT_LOCK;
77
    }
17 ilm 78
 
73 ilm 79
    public static final void setDefaultLockSelect(final boolean b) {
80
        DEFAULT_SELECT_LOCK = b;
81
    }
82
 
20 ilm 83
    static public void setupForeign(final SQLRowValuesListFetcher fetcher) {
84
        // include rows having NULL (not undefined ID) foreign keys
85
        fetcher.setFullOnly(false);
86
        // treat the same way tables with or without undefined ID
87
        fetcher.setIncludeForeignUndef(false);
73 ilm 88
        // be predictable
89
        fetcher.setReferentsOrdered(true, true);
20 ilm 90
    }
91
 
142 ilm 92
    static public final boolean addToFetch(final SQLRowValues input, final Path p, final Collection<String> fields) {
93
        assert p == null || p.isSingleLink() : "Graph size not sufficient to know if graph was modified";
94
        final int graphSize = input.getGraphSize();
93 ilm 95
        // don't back track : e.g. if path is SITE -> CLIENT <- SITE we want the siblings of SITE,
96
        // if we want fields of the primary SITE we pass the path SITE
97
        final SQLRowValues r = p == null ? input : input.followPathToOne(p, CreateMode.CREATE_ONE, false);
142 ilm 98
        boolean modified = input.getGraphSize() > graphSize;
93 ilm 99
        for (final String f : fields) {
142 ilm 100
            // don't overwrite foreign rows and update modified
101
            if (!r.getFields().contains(f)) {
93 ilm 102
                r.put(f, null);
142 ilm 103
                modified = true;
104
            }
93 ilm 105
        }
142 ilm 106
        return modified;
93 ilm 107
    }
108
 
17 ilm 109
    private final SQLTable primaryTable;
93 ilm 110
    @GuardedBy("this")
142 ilm 111
    private List<Path> order;
112
    @GuardedBy("this")
182 ilm 113
    private Map<Object, Where> wheres;
114
    @GuardedBy("this")
17 ilm 115
    private Where where;
83 ilm 116
    @GuardedBy("this")
142 ilm 117
    private Map<IFieldPath, SearchField> searchFields;
83 ilm 118
    @GuardedBy("this")
142 ilm 119
    private int searchLimit;
93 ilm 120
    @GuardedBy("this")
17 ilm 121
    private ITransformer<SQLSelect, SQLSelect> selTransf;
93 ilm 122
    @GuardedBy("this")
73 ilm 123
    private boolean lockSelect;
17 ilm 124
 
142 ilm 125
    private final SQLRowValues graph;
93 ilm 126
    @GuardedBy("this")
17 ilm 127
    private SQLRowValues graphToFetch;
128
 
93 ilm 129
    @GuardedBy("this")
130
    private SQLRowValuesListFetcher frozen;
131
 
132
    {
133
        // a new instance is never frozen
134
        this.frozen = null;
135
    }
136
 
17 ilm 137
    private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
138
 
142 ilm 139
    public BaseFillSQLRequest(final SQLRowValues graph, final Where w) {
17 ilm 140
        super();
142 ilm 141
        if (graph == null)
17 ilm 142
            throw new NullPointerException();
142 ilm 143
        this.primaryTable = graph.getTable();
144
        this.setOrder(null);
182 ilm 145
        this.setWhere(w);
83 ilm 146
        this.searchFields = Collections.emptyMap();
142 ilm 147
        this.searchLimit = 35;
17 ilm 148
        this.selTransf = null;
73 ilm 149
        this.lockSelect = getDefaultLockSelect();
142 ilm 150
        this.graph = graph.toImmutable();
17 ilm 151
        this.graphToFetch = null;
152
    }
153
 
61 ilm 154
    public BaseFillSQLRequest(final BaseFillSQLRequest req) {
17 ilm 155
        super();
156
        this.primaryTable = req.getPrimaryTable();
93 ilm 157
        synchronized (req) {
142 ilm 158
            this.order = req.order;
182 ilm 159
            this.wheres = req.wheres;
93 ilm 160
            this.where = req.where;
161
            this.searchFields = req.searchFields;
142 ilm 162
            this.searchLimit = req.searchLimit;
93 ilm 163
            this.selTransf = req.selTransf;
164
            this.lockSelect = req.lockSelect;
165
            // use methods since they're both lazy
166
            this.graph = req.getGraph();
167
            this.graphToFetch = req.getGraphToFetch();
168
        }
17 ilm 169
    }
170
 
93 ilm 171
    public synchronized final boolean isFrozen() {
172
        return this.frozen != null;
173
    }
174
 
175
    public final void freeze() {
176
        this.freeze(this);
177
    }
178
 
179
    private final synchronized void freeze(final BaseFillSQLRequest from) {
180
        if (!this.isFrozen()) {
181
            // compute the fetcher once and for all
182
            this.frozen = from.getFetcher();
183
            assert this.frozen.isFrozen();
184
            this.wasFrozen();
185
        }
186
    }
187
 
188
    protected void wasFrozen() {
189
    }
190
 
191
    protected final void checkFrozen() {
192
        if (this.isFrozen())
193
            throw new IllegalStateException("this has been frozen: " + this);
194
    }
195
 
196
    // not final so we can narrow down the return type
197
    public BaseFillSQLRequest toUnmodifiable() {
198
        return this.toUnmodifiableP(this.getClass());
199
    }
200
 
201
    // should be passed the class created by cloneForFreeze(), i.e. not this.getClass() or this
202
    // won't support anonymous classes
203
    protected final <T extends BaseFillSQLRequest> T toUnmodifiableP(final Class<T> clazz) {
204
        final Class<? extends BaseFillSQLRequest> thisClass = this.getClass();
205
        if (clazz != thisClass && !(thisClass.isAnonymousClass() && clazz == thisClass.getSuperclass()))
206
            throw new IllegalArgumentException("Passed class isn't our class : " + clazz + " != " + thisClass);
207
        final BaseFillSQLRequest res;
208
        synchronized (this) {
209
            if (this.isFrozen()) {
210
                res = this;
211
            } else {
212
                res = this.clone(true);
213
                if (res.getClass() != clazz)
214
                    throw new IllegalStateException("Clone class mismatch : " + res.getClass() + " != " + clazz);
215
                // freeze before releasing lock (even if not recommended, allow to modify the state
216
                // of getSelectTransf() while holding our lock)
217
                // pass ourselves so that if we are an anonymous class the fetcher created with our
218
                // overloaded methods is used
219
                res.freeze(this);
220
            }
221
        }
222
        assert res.getClass() == clazz || res.getClass().getSuperclass() == clazz;
223
        @SuppressWarnings("unchecked")
224
        final T casted = (T) res;
225
        return casted;
226
    }
227
 
228
    // must be called with our lock
229
    protected abstract BaseFillSQLRequest clone(boolean forFreeze);
230
 
142 ilm 231
    static protected final SQLRowValues computeGraph(final SQLTable t, final Collection<String> fields, final FieldExpander exp) {
232
        final SQLRowValues vals = new SQLRowValues(t).putNulls(fields);
233
        exp.expand(vals);
93 ilm 234
        return vals.toImmutable();
17 ilm 235
    }
236
 
237
    /**
142 ilm 238
     * The graph with fields to be automatically added to the UI.
17 ilm 239
     *
93 ilm 240
     * @return the expanded frozen graph.
17 ilm 241
     */
242
    public final SQLRowValues getGraph() {
142 ilm 243
        return this.graph;
17 ilm 244
    }
245
 
246
    /**
93 ilm 247
     * The graph to fetch, should be a superset of {@link #getGraph()}. To modify it, see
248
     * {@link #addToGraphToFetch(Path, Set)} and {@link #changeGraphToFetch(IClosure)}.
17 ilm 249
     *
93 ilm 250
     * @return the graph to fetch, frozen.
17 ilm 251
     */
252
    public final SQLRowValues getGraphToFetch() {
93 ilm 253
        synchronized (this) {
254
            if (this.graphToFetch == null && this.getGraph() != null) {
255
                assert !this.isFrozen() : "no computation should take place after frozen()";
256
                final SQLRowValues tmp = this.getGraph().deepCopy();
257
                this.customizeToFetch(tmp);
142 ilm 258
                this.setGraphToFetch(tmp, true);
93 ilm 259
            }
260
            return this.graphToFetch;
17 ilm 261
        }
262
    }
263
 
132 ilm 264
    public final void addToGraphToFetch(final String... fields) {
265
        this.addToGraphToFetch(Arrays.asList(fields));
266
    }
267
 
93 ilm 268
    public final void addToGraphToFetch(final Collection<String> fields) {
269
        this.addToGraphToFetch(null, fields);
270
    }
271
 
132 ilm 272
    public final void addForeignToGraphToFetch(final String foreignField, final Collection<String> fields) {
273
        this.addToGraphToFetch(new Path(getPrimaryTable()).addForeignField(foreignField), fields);
274
    }
275
 
93 ilm 276
    /**
277
     * Make sure that the fields at the end of the path are fetched.
278
     *
279
     * @param p a path.
280
     * @param fields fields to fetch.
281
     */
282
    public final void addToGraphToFetch(final Path p, final Collection<String> fields) {
283
        this.changeGraphToFetch(new IClosure<SQLRowValues>() {
284
            @Override
285
            public void executeChecked(SQLRowValues input) {
286
                addToFetch(input, p, fields);
287
            }
142 ilm 288
        }, false);
93 ilm 289
    }
290
 
291
    public final void changeGraphToFetch(IClosure<SQLRowValues> cl) {
142 ilm 292
        this.changeGraphToFetch(cl, true);
293
    }
294
 
295
    private final void changeGraphToFetch(IClosure<SQLRowValues> cl, final boolean checkNeeded) {
93 ilm 296
        synchronized (this) {
297
            checkFrozen();
298
            final SQLRowValues tmp = this.getGraphToFetch().deepCopy();
299
            cl.executeChecked(tmp);
142 ilm 300
            this.setGraphToFetch(tmp, checkNeeded);
93 ilm 301
        }
142 ilm 302
        fireWhereChange();
93 ilm 303
    }
304
 
142 ilm 305
    private final void setGraphToFetch(final SQLRowValues tmp, final boolean checkNeeded) {
306
        assert Thread.holdsLock(this) && !this.isFrozen();
307
        if (checkNeeded && !tmp.graphContains(this.getGraph()))
308
            throw new IllegalArgumentException("New graph too small");
309
        this.graphToFetch = tmp.toImmutable();
310
    }
311
 
61 ilm 312
    protected void customizeToFetch(final SQLRowValues graphToFetch) {
17 ilm 313
    }
314
 
93 ilm 315
    protected synchronized final SQLRowValuesListFetcher getFetcher() {
316
        if (this.isFrozen())
317
            return this.frozen;
142 ilm 318
        // fetch order fields, so that consumers can order an updated row in an existing list
319
        final SQLRowValues tmp = getGraphToFetch().deepCopy();
320
        for (final Path orderP : this.getOrder()) {
321
            final SQLRowValues orderVals = tmp.followPath(orderP);
322
            if (orderVals != null && orderVals.getTable().isOrdered()) {
323
                orderVals.put(orderVals.getTable().getOrderField().getName(), null);
324
            }
325
        }
61 ilm 326
        // graphToFetch can be modified freely so don't the use the simple constructor
142 ilm 327
        // order to have predictable result (this will both order the referent rows and main rows.
328
        // The latter will be overwritten by our own getOrder())
329
        return setupFetcher(SQLRowValuesListFetcher.create(tmp, true));
67 ilm 330
    }
331
 
332
    // allow to pass fetcher since they are mostly immutable (and for huge graphs they are slow to
333
    // create)
93 ilm 334
    protected final SQLRowValuesListFetcher setupFetcher(final SQLRowValuesListFetcher fetcher) {
67 ilm 335
        final String tableName = getPrimaryTable().getName();
20 ilm 336
        setupForeign(fetcher);
93 ilm 337
        synchronized (this) {
338
            fetcher.setOrder(getOrder());
339
            fetcher.setReturnedRowsUnmodifiable(true);
132 ilm 340
            fetcher.appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
93 ilm 341
                @Override
342
                public SQLSelect transformChecked(SQLSelect sel) {
343
                    sel = transformSelect(sel);
344
                    if (isLockSelect())
132 ilm 345
                        sel.addLockedTable(tableName);
93 ilm 346
                    return sel.andWhere(getWhere());
347
                }
348
            });
349
            // freeze to execute setSelTransf() before leaving the synchronized block
350
            fetcher.freeze();
351
        }
17 ilm 352
        return fetcher;
353
    }
354
 
142 ilm 355
    protected synchronized final List<Path> getOrder() {
356
        if (this.order != null)
357
            return this.order;
358
        return this.getDefaultOrder();
359
    }
360
 
151 ilm 361
    public final boolean isTableOrder() {
362
        return this.getPrimaryTable().isOrdered() && this.getOrder().equals(getTableOrder());
363
    }
364
 
365
    /**
366
     * Order the passed rows the same as {@link #getFetcher()}.
367
     *
368
     * @param r1 the first row.
369
     * @param r2 the second row.
370
     * @return a negative integer, zero, or a positive integer as the first argument is less than,
371
     *         equal to, or greater than the second.
372
     */
373
    public final int order(final SQLRowValues r1, final SQLRowValues r2) {
374
        if (r1 == r2)
375
            return 0;
376
        // same behaviour as SQLSelect
377
        final Comparator<SQLRowAccessor> comp = OrderComparator.getFallbackToPKInstance();
378
        for (final Path p : getOrder()) {
379
            final SQLRowValues o1 = r1.followPath(p);
380
            final SQLRowValues o2 = r2.followPath(p);
381
            final int res = comp.compare(o1, o2);
382
            if (res != 0)
383
                return res;
384
        }
385
        return 0;
386
    }
387
 
182 ilm 388
    static private final VirtualFields FIELDS_FOR_ORDER = VirtualFields.PRIMARY_KEY.union(VirtualFields.ORDER);
389
 
390
    // allow to save memory by only keeping trimmed SQLRow
391
    public final OrderValue createOrderValue(final SQLRowValues r) {
392
        if (!r.isFrozen())
393
            throw new IllegalArgumentException("Row not frozen : " + r);
394
        final List<Path> order = getOrder();
395
        final List<SQLRow> rows = new ArrayList<>(order.size());
396
        for (final Path p : order) {
397
            rows.add(r.followPath(p).trimmedRow(FIELDS_FOR_ORDER));
398
        }
399
        return new OrderValue(Collections.unmodifiableList(rows));
400
    }
401
 
402
    static public final class OrderValue implements Comparable<OrderValue> {
403
        private final List<SQLRow> rows;
404
 
405
        OrderValue(List<SQLRow> rows) {
406
            super();
407
            this.rows = rows;
408
        }
409
 
410
        @Override
411
        public int compareTo(OrderValue o2) {
412
            if (this == o2)
413
                return 0;
414
            final int size = this.rows.size();
415
            if (size != o2.rows.size())
416
                throw new IllegalArgumentException("Not same state");
417
            // same behaviour as SQLSelect
418
            final Comparator<SQLRowAccessor> comp = OrderComparator.getFallbackToPKInstance();
419
            for (int i = 0; i < size; i++) {
420
                final SQLRow r1 = this.rows.get(i);
421
                final SQLRow r2 = o2.rows.get(i);
422
                final int res = comp.compare(r1, r2);
423
                if (res != 0)
424
                    return res;
425
            }
426
            return 0;
427
        }
428
    }
429
 
142 ilm 430
    protected List<Path> getDefaultOrder() {
151 ilm 431
        return getTableOrder();
432
    }
433
 
434
    protected final List<Path> getTableOrder() {
80 ilm 435
        return Collections.singletonList(Path.get(getPrimaryTable()));
17 ilm 436
    }
437
 
142 ilm 438
    /**
439
     * Change the ordering of this request.
440
     *
441
     * @param l the list of tables, <code>null</code> to restore the {@link #getDefaultOrder()
442
     *        default} .
443
     */
444
    public synchronized final void setOrder(List<Path> l) {
445
        checkFrozen();
446
        this.order = l == null ? null : Collections.unmodifiableList(new ArrayList<Path>(l));
447
    }
448
 
182 ilm 449
    /**
450
     * Set a where to be AND'd.
451
     *
452
     * @param o a key, <code>null</code> to change the where set by {@link #setWhere(Where)}.
453
     * @param w the new value, <code>null</code> to remove.
454
     */
455
    public final void putWhere(final Object o, final Where w) {
456
        synchronized (this) {
457
            checkFrozen();
458
            final Map<Object, Where> newValue = new HashMap<>(this.wheres);
459
            if (w == null)
460
                newValue.remove(o);
461
            else
462
                newValue.put(o, w);
463
            this.wheres = Collections.unmodifiableMap(newValue);
464
            this.where = Where.and(this.wheres.values());
465
        }
466
        fireWhereChange();
467
    }
468
 
469
    /**
470
     * Set the where, replacing any other set by {@link #putWhere(Object, Where)}.
471
     *
472
     * @param w the new value, <code>null</code> to remove.
473
     */
61 ilm 474
    public final void setWhere(final Where w) {
93 ilm 475
        synchronized (this) {
476
            checkFrozen();
182 ilm 477
            this.wheres = w == null ? Collections.<Object, Where> emptyMap() : Collections.singletonMap(null, w);
93 ilm 478
            this.where = w;
479
        }
17 ilm 480
        fireWhereChange();
481
    }
482
 
93 ilm 483
    public synchronized final Where getWhere() {
17 ilm 484
        return this.where;
485
    }
486
 
83 ilm 487
    /**
488
     * Whether this request is searchable.
489
     *
490
     * @param b <code>true</code> if the {@link #getFields() local fields} should be used,
491
     *        <code>false</code> to not be searchable.
492
     */
493
    public final void setSearchable(final boolean b) {
142 ilm 494
        this.setSearchFields(b ? getDefaultSearchFields() : Collections.<SearchField> emptyList());
83 ilm 495
    }
496
 
142 ilm 497
    protected Collection<SearchField> getDefaultSearchFields() {
498
        final Set<String> names = CollectionUtils.inter(this.getGraph().getFields(), this.getPrimaryTable().getFieldsNames(VirtualFields.LOCAL_CONTENT));
499
        return mapOfModesToSearchFields(CollectionUtils.<String, SQLSearchMode> createMap(names));
500
    }
501
 
83 ilm 502
    /**
503
     * Set the fields used to search.
504
     *
505
     * @param searchFields only rows with these fields containing the terms will match.
506
     * @see #setSearch(String)
507
     */
142 ilm 508
    public final void setSearchFieldsNames(final Collection<String> searchFields) {
509
        this.setSearchFieldsNames(CollectionUtils.<String, SQLSearchMode> createMap(searchFields));
83 ilm 510
    }
511
 
142 ilm 512
    protected final Collection<SearchField> mapOfModesToSearchFields(Map<String, SQLSearchMode> searchFields) {
513
        final List<SearchField> list = new ArrayList<SearchField>();
514
        for (final Entry<String, SQLSearchMode> e : searchFields.entrySet()) {
515
            list.add(new SearchField(getPrimaryTable().getField(e.getKey()), e.getValue() == null ? SQLSearchMode.CONTAINS : e.getValue()));
516
        }
517
        return list;
518
    }
519
 
83 ilm 520
    /**
521
     * Set the fields used to search.
522
     *
523
     * @param searchFields for each field to search, how to match.
524
     * @see #setSearch(String)
525
     */
142 ilm 526
    public final void setSearchFieldsNames(Map<String, SQLSearchMode> searchFields) {
527
        this.setSearchFields(mapOfModesToSearchFields(searchFields));
528
    }
529
 
530
    public final void setSearchFields(final Collection<SearchField> searchFields) {
93 ilm 531
        // can be outside the synchronized block, since it can't be reverted
532
        checkFrozen();
142 ilm 533
        final Map<IFieldPath, SearchField> copy = new HashMap<IFieldPath, SearchField>();
534
        for (final SearchField f : searchFields) {
535
            final SearchField prev = copy.put(f.getField(), f);
536
            if (prev != null)
537
                throw new IllegalArgumentException("Duplicate : " + f.getField());
83 ilm 538
        }
539
        synchronized (this) {
142 ilm 540
            this.searchFields = Collections.unmodifiableMap(copy);
83 ilm 541
        }
542
        fireWhereChange();
543
    }
544
 
142 ilm 545
    public Map<IFieldPath, SearchField> getSearchFields() {
83 ilm 546
        synchronized (this) {
547
            return this.searchFields;
548
        }
549
    }
550
 
142 ilm 551
    public synchronized final boolean isSearchable() {
552
        return !this.getSearchFields().isEmpty();
83 ilm 553
    }
554
 
142 ilm 555
    public synchronized final void setSearchLimit(final int limit) {
556
        this.searchLimit = limit;
557
    }
558
 
559
    public synchronized final int getSearchLimit() {
560
        return this.searchLimit;
561
    }
562
 
93 ilm 563
    public final synchronized void setLockSelect(boolean lockSelect) {
564
        checkFrozen();
73 ilm 565
        this.lockSelect = lockSelect;
566
    }
567
 
93 ilm 568
    public final synchronized boolean isLockSelect() {
73 ilm 569
        return this.lockSelect;
570
    }
571
 
142 ilm 572
    public Set<SQLTable> getTables() {
573
        final Set<SQLTable> res = new HashSet<SQLTable>();
574
        for (final SQLRowValues v : this.getGraphToFetch().getGraph().getItems())
575
            res.add(v.getTable());
576
        return res;
17 ilm 577
    }
578
 
142 ilm 579
    public final void addTableListener(SQLTableModifiedListener l) {
580
        for (final SQLTable t : this.getTables()) {
581
            t.addTableModifiedListener(l);
582
        }
583
    }
17 ilm 584
 
142 ilm 585
    public final void removeTableListener(SQLTableModifiedListener l) {
586
        for (final SQLTable t : this.getTables()) {
587
            t.removeTableModifiedListener(l);
588
        }
589
    }
590
 
591
    protected final List<SQLField> getFields() {
592
        return this.getPrimaryTable().getFields(this.getGraph().getFields());
593
    }
594
 
61 ilm 595
    protected SQLSelect transformSelect(final SQLSelect sel) {
142 ilm 596
        final ITransformer<SQLSelect, SQLSelect> transf = this.getSelectTransf();
597
        return transf == null ? sel : transf.transformChecked(sel);
598
    }
599
 
600
    // @param searchQuery null means don't want to search in SQL (i.e. no WHERE, no LIMIT), empty
601
    // means nothing to search (i.e. no WHERE but LIMIT).
602
    protected final ITransformer<SQLSelect, SQLSelect> createSearchTransformer(final List<String> searchQuery, final Locale l, final Where forceInclude) {
603
        if (searchQuery == null)
604
            return null;
605
        final Map<IFieldPath, SearchField> searchFields;
606
        final int searchLimit;
607
        final boolean searchable;
83 ilm 608
        synchronized (this) {
609
            searchFields = this.getSearchFields();
142 ilm 610
            searchLimit = this.getSearchLimit();
611
            searchable = this.isSearchable();
83 ilm 612
        }
142 ilm 613
        if (!searchable) {
614
            throw new IllegalArgumentException("Cannot search " + searchQuery);
615
        }
616
        // continue even if searchQuery is empty to apply the LIMIT
617
        final List<String> immutableQuery = Collections.unmodifiableList(new ArrayList<String>(searchQuery));
618
        return new ITransformer<SQLSelect, SQLSelect>() {
619
            @Override
620
            public SQLSelect transformChecked(SQLSelect sel) {
621
                return transformSelectSearch(sel, searchFields, searchLimit, immutableQuery, l, forceInclude);
622
            }
623
        };
624
    }
625
 
626
    static protected final SQLSelect transformSelectSearch(final SQLSelect sel, final Map<IFieldPath, SearchField> searchFields, final int searchLimit, final List<String> searchQuery, final Locale l,
627
            final Where forceInclude) {
83 ilm 628
        final Where w;
629
        final Set<String> matchScore = new HashSet<String>();
142 ilm 630
        if (!searchQuery.isEmpty()) {
631
            final SQLSyntax syntax = sel.getSyntax();
83 ilm 632
            Where where = null;
633
            for (final String searchTerm : searchQuery) {
634
                Where termWhere = null;
142 ilm 635
                for (final SearchField searchField : searchFields.values()) {
636
                    final FieldRef selF = sel.followFieldPath(searchField.getField());
637
                    final SQLSearchMode mode = searchField.getMode();
638
                    final List<String> formatted = searchField.format(selF, l);
639
                    final String fieldWhere = createWhere(syntax, formatted, mode, searchTerm);
640
                    termWhere = Where.createRaw(fieldWhere).or(termWhere);
641
                    if (searchField.getScore() > 0 || !searchField.getHigherModes().isEmpty()) {
642
                        final CaseBuilder caseBuilder = syntax.createCaseWhenBuilder().setElse("0");
643
                        for (final Tuple2<SQLSearchMode, Integer> hm : searchField.getHigherModes()) {
644
                            caseBuilder.addWhen(createWhere(syntax, formatted, hm.get0(), searchTerm), String.valueOf(hm.get1()));
645
                        }
646
                        if (searchField.getScore() > 0) {
647
                            caseBuilder.addWhen(fieldWhere, String.valueOf(searchField.getScore()));
648
                        }
649
                        matchScore.add(caseBuilder.build());
83 ilm 650
                    }
651
                }
652
                where = Where.and(termWhere, where);
653
            }
142 ilm 654
            // only use forceInclude when there's a restriction otherwise the include transforms
655
            // itself into a restrict
656
            if (where != null)
657
                where = where.or(forceInclude);
83 ilm 658
            w = where;
659
        } else {
660
            w = null;
661
        }
662
        sel.andWhere(w);
142 ilm 663
        if (forceInclude != null)
664
            matchScore.add("case when " + forceInclude + " then 10000 else 0 end");
83 ilm 665
        if (!matchScore.isEmpty())
666
            sel.getOrder().add(0, CollectionUtils.join(matchScore, " + ") + " DESC");
142 ilm 667
        if (searchLimit >= 0)
668
            sel.setLimit(searchLimit);
83 ilm 669
 
142 ilm 670
        return sel;
17 ilm 671
    }
672
 
142 ilm 673
    static protected final String createWhere(final SQLSyntax syntax, final List<String> formatted, final SQLSearchMode mode, final String searchQuery) {
674
        return CollectionUtils.join(formatted, " OR ", new ITransformer<String, String>() {
675
            @Override
676
            public String transformChecked(String sqlExpr) {
677
                return createWhere(sqlExpr, mode, syntax, searchQuery);
678
            }
679
        });
83 ilm 680
    }
681
 
142 ilm 682
    static public final List<String> defaultFormat(final FieldRef selF, final Locale l) {
683
        final SQLType type = selF.getField().getType();
684
        final SQLSyntax syntax = SQLSyntax.get(selF.getField());
685
        if (type.getJavaType() == String.class) {
686
            return Collections.singletonList(selF.getFieldRef());
687
        } else if (type.getJavaType() == Boolean.class) {
177 ilm 688
            final org.openconcerto.utils.i18n.TM utilsTM = org.openconcerto.utils.i18n.TM.getInstance(l);
689
            return Collections.singletonList(
690
                    "case when " + selF.getFieldRef() + " then " + syntax.quoteString(utilsTM.translate("true_key")) + " else " + syntax.quoteString(utilsTM.translate("false_key")) + " end");
142 ilm 691
        } else if (Timestamp.class.isAssignableFrom(type.getJavaType())) {
692
            final String shortFmt = formatTime(selF, DateProp.SHORT_DATETIME_SKELETON, l, syntax);
693
            final String longFmt = formatTime(selF, DateProp.LONG_DATETIME_SKELETON, l, syntax);
694
            return Arrays.asList(shortFmt, longFmt);
695
        } else if (Time.class.isAssignableFrom(type.getJavaType())) {
696
            return Collections.singletonList(formatTime(selF, DateProp.TIME_SKELETON, l, syntax));
697
        } else if (Date.class.isAssignableFrom(type.getJavaType())) {
698
            final String shortFmt = formatTime(selF, DateProp.SHORT_DATE_SKELETON, l, syntax);
699
            final String longFmt = formatTime(selF, DateProp.LONG_DATE_SKELETON, l, syntax);
700
            return Arrays.asList(shortFmt, longFmt);
701
        } else {
702
            return Collections.singletonList(syntax.cast(selF.getFieldRef(), String.class));
703
        }
704
    }
705
 
706
    static public final String formatTime(final FieldRef selF, final List<String> simpleFormat, final Locale l, final SQLSyntax syntax) {
707
        return syntax.getFormatTimestampSimple(selF.getFieldRef(), DateProp.getBestPattern(simpleFormat, l), l);
708
    }
709
 
710
    static protected final String createWhere(final String sqlExpr, final SQLSearchMode mode, final SQLSyntax syntax, final String searchQuery) {
711
        return "lower(" + sqlExpr + ") " + mode.generateSQL(syntax, searchQuery.toLowerCase());
712
    }
713
 
714
    static public class SearchField {
715
        private final IFieldPath field;
716
        private final SQLSearchMode mode;
717
        private final int score;
718
        private final List<Tuple2<SQLSearchMode, Integer>> higherModes;
719
 
720
        public SearchField(IFieldPath field, SQLSearchMode mode) {
721
            this(field, mode, 1);
722
        }
723
 
724
        /**
725
         * Create a new search field.
726
         *
727
         * @param field which field to search.
728
         * @param mode how to search.
729
         * @param score the score (>0) to attribute if the field matches. Allow to rank fields
730
         *        between themselves.
731
         */
732
        public SearchField(IFieldPath field, SQLSearchMode mode, int score) {
733
            this(field, mode, score, -1, -1);
734
        }
735
 
736
        public SearchField(final IFieldPath field, final SQLSearchMode mode, final int score, final int score2, final int score3) {
737
            super();
738
            if (field.getField().getFieldGroup().getKeyType() != null)
739
                throw new IllegalArgumentException("Field is a key : " + field);
740
            this.field = field;
741
            this.mode = mode;
742
            /*
743
             * for now we could pass <code>1</code> so that a row with more matches is higher ranked
744
             * (e.g. if searching "a" ["ant", "cat"] is better than ["ant", "horse"]), or
745
             * <code>0</code> to ignore the match count. But this only works because we have
746
             * separate WHERE and ORDER BY ; if we had a computed column with "WHERE score > 0 ORDER
747
             * BY score" this would be complicated.
748
             */
749
            if (score < 1)
750
                throw new IllegalArgumentException("Invalid score : " + score);
751
            this.score = score;
752
            final List<SQLSearchMode> higherModes = field.getField().getType().getJavaType() == String.class ? this.mode.getHigherModes() : Collections.<SQLSearchMode> emptyList();
753
            if (higherModes.isEmpty()) {
754
                this.higherModes = Collections.emptyList();
755
            } else {
756
                if (higherModes.size() > 2)
757
                    throw new IllegalStateException("Too many higher modes " + higherModes);
758
                final List<Tuple2<SQLSearchMode, Integer>> tmp = new ArrayList<Tuple2<SQLSearchMode, Integer>>(2);
759
                tmp.add(Tuple2.create(higherModes.get(0), score3 < 1 ? Math.max((int) (this.score * 1.5), this.score + 2) : score3));
760
                if (higherModes.size() > 1)
761
                    tmp.add(Tuple2.create(higherModes.get(1), score2 < 1 ? Math.max((int) (this.score * 1.2), this.score + 1) : score2));
762
                this.higherModes = Collections.unmodifiableList(tmp);
763
            }
764
        }
765
 
766
        public final IFieldPath getField() {
767
            return this.field;
768
        }
769
 
770
        public final SQLSearchMode getMode() {
771
            return this.mode;
772
        }
773
 
774
        public final int getScore() {
775
            return this.score;
776
        }
777
 
778
        public List<Tuple2<SQLSearchMode, Integer>> getHigherModes() {
779
            return this.higherModes;
780
        }
781
 
782
        protected List<String> format(final FieldRef selF, final Locale l) {
783
            if (getField().getField() != selF.getField())
784
                throw new IllegalArgumentException("Wrong field");
785
            return defaultFormat(selF, l);
786
        }
787
    }
788
 
93 ilm 789
    public final synchronized ITransformer<SQLSelect, SQLSelect> getSelectTransf() {
17 ilm 790
        return this.selTransf;
791
    }
792
 
793
    /**
794
     * Allows to transform the SQLSelect returned by getFillRequest().
795
     *
93 ilm 796
     * @param transf the transformer to apply, needs to be thread-safe.
17 ilm 797
     */
61 ilm 798
    public final void setSelectTransf(final ITransformer<SQLSelect, SQLSelect> transf) {
93 ilm 799
        synchronized (this) {
800
            checkFrozen();
801
            this.selTransf = transf;
802
        }
17 ilm 803
        this.fireWhereChange();
804
    }
805
 
806
    public final SQLTable getPrimaryTable() {
807
        return this.primaryTable;
808
    }
809
 
810
    protected final void fireWhereChange() {
93 ilm 811
        // don't call unknown code with our lock
812
        assert !Thread.holdsLock(this);
17 ilm 813
        this.supp.firePropertyChange("where", null, null);
814
    }
815
 
61 ilm 816
    public final void addWhereListener(final PropertyChangeListener l) {
17 ilm 817
        this.supp.addPropertyChangeListener("where", l);
818
    }
819
 
61 ilm 820
    public final void rmWhereListener(final PropertyChangeListener l) {
17 ilm 821
        this.supp.removePropertyChangeListener("where", l);
822
    }
823
 
61 ilm 824
    @Override
17 ilm 825
    public String toString() {
826
        return this.getClass().getName() + " on " + this.getPrimaryTable();
827
    }
828
}