OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 151 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

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