OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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