OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.view.search;
15
 
16
import org.openconcerto.utils.FormatGroup;
17
 
18
import java.text.Format;
19
import java.text.Normalizer;
20
import java.text.Normalizer.Form;
28 ilm 21
import java.text.ParsePosition;
17 ilm 22
import java.util.HashMap;
23
import java.util.List;
24
import java.util.Map;
25
import java.util.regex.Pattern;
26
 
27
/**
28
 * Search a text in an object (first in toString() then using formats).
29
 *
30
 * @author Sylvain
31
 */
32
public class TextSearchSpec implements SearchSpec {
33
 
34
    public static enum Mode {
35
        CONTAINS, CONTAINS_STRICT, LESS_THAN, EQUALS, EQUALS_STRICT, GREATER_THAN
36
    }
37
 
38
    // cannot use Collator : it doesn't works for CONTAINS
39
    static private final Pattern thoroughPattern = Pattern.compile("(\\p{Punct}|\\p{InCombiningDiacriticalMarks})+");
40
    static private final Pattern multipleSpacesPattern = Pattern.compile("\\p{Space}+");
41
 
42
    private final Mode mode;
43
    private final String filterString, normalizedFilterString;
44
    private final Map<Class<?>, FormatGroup> formats;
45
    // parsing of filterString for each format
46
    private final Map<Format, Object> parsedFilter;
47
    private Double parsedFilterD;
48
    private boolean parsedFilterD_tried = false;
49
 
50
    public TextSearchSpec(String filterString) {
51
        this(filterString, Mode.CONTAINS);
52
    }
53
 
54
    public TextSearchSpec(String filterString, final Mode mode) {
55
        this.mode = mode;
56
        this.filterString = filterString;
57
        this.normalizedFilterString = normalize(filterString);
182 ilm 58
        this.formats = new HashMap<>();
59
        this.parsedFilter = new HashMap<>();
17 ilm 60
    }
61
 
62
    private String normalize(String s) {
63
        if (this.mode == Mode.CONTAINS_STRICT || this.mode == Mode.EQUALS_STRICT) {
64
            return s.trim();
65
        } else {
66
            final String sansAccents = thoroughPattern.matcher(Normalizer.normalize(s.trim(), Form.NFD)).replaceAll("");
67
            return multipleSpacesPattern.matcher(sansAccents).replaceAll(" ").toLowerCase();
68
        }
69
    }
70
 
71
    private final Object getParsed(final Format fmt) {
72
        Object res;
73
        if (this.parsedFilter.containsKey(fmt)) {
74
            res = this.parsedFilter.get(fmt);
75
        } else {
28 ilm 76
            final ParsePosition pp = new ParsePosition(0);
77
            res = fmt.parseObject(this.filterString, pp);
78
            // don't allow "25/12/05foobar" or worse "25/12/05 13:00" (parsing to "25/12/05 00:00")
79
            if (pp.getErrorIndex() >= 0 || pp.getIndex() < this.filterString.length())
80
                res = null;
81
            else
17 ilm 82
                assert res != null : "Cannot tell apart parsing failed from parsed to null";
83
            this.parsedFilter.put(fmt, res);
84
        }
85
        return res;
86
    }
87
 
156 ilm 88
    private final String format(final Format fmt, final Object cell) {
89
        try {
90
            return fmt.format(cell);
182 ilm 91
        } catch (final Exception e) {
156 ilm 92
            throw new IllegalStateException("Couldn't format " + cell + '(' + getClass(cell) + ") with " + fmt, e);
93
        }
94
    }
95
 
96
    static private String getClass(Object cell) {
97
        return cell == null ? "<null>" : cell.getClass().getName();
98
    }
99
 
17 ilm 100
    private final Double getDouble() {
101
        if (!this.parsedFilterD_tried) {
102
            try {
103
                this.parsedFilterD = Double.valueOf(this.filterString);
182 ilm 104
            } catch (final NumberFormatException e) {
17 ilm 105
                this.parsedFilterD = null;
106
            }
107
            this.parsedFilterD_tried = true;
108
        }
109
        return this.parsedFilterD;
110
    }
111
 
112
    private boolean matchWithFormats(Object cell) {
113
        if (cell == null)
114
            return false;
115
 
116
        // return now since only the toString() of strings can be sorted (it makes no sense to sort
117
        // 12/25/2010)
118
        if (cell.getClass() == String.class)
119
            return test(cell.toString());
120
 
121
        final boolean containsOrEquals = isContainsOrEquals();
122
        final boolean isContains = isContains();
123
 
124
        // first an inexpensive comparison
125
        if (containsOrEquals && containsOrEquals(cell.toString()))
126
            return true;
127
 
128
        // then try to format the cell
129
        final FormatGroup fg = getFormat(cell);
130
        if (fg != null) {
131
            final List<? extends Format> fmts = fg.getFormats();
132
            final int stop = fmts.size();
133
            for (int i = 0; i < stop; i++) {
134
                final Format fmt = fmts.get(i);
135
                // e.g. test if "2006" is contained in "25 déc. 2010"
28 ilm 136
 
137
                // for the equals mode we can either format the cell value and compare it to the
138
                // user typed string, or we can parse the user typed string and compare it to the
139
                // cell value.
140
                // The problem with the first approach is that some formats lose information (e.g. a
141
                // date format for a timestamp loses the time part : 25/12/2010 13:00 would be
142
                // formatted to 25/12/2010 thus "= 25/12/2010" would select all times of that day,
143
                // which is better achieved with contains)
144
                // PS: the date format is useful for parsing since "> 25/12/2010" means
145
                // "> 25/12/2010 00:00" which is understandable and concise.
156 ilm 146
                if (isContains && containsOrEquals(format(fmt, cell)))
17 ilm 147
                    return true;
148
                // e.g. test if "01/01/2006" is before "25 déc. 2010"
149
                else if (!isContains && test(getParsed(fmt), cell))
150
                    return true;
151
            }
152
        } else if (!isContains && cell instanceof Number) {
153
            final Number n = (Number) cell;
154
            if (test(this.getDouble(), n.doubleValue()))
155
                return true;
156
        }
157
        return false;
158
    }
159
 
160
    private boolean test(final String searched) {
161
        final String normalized = normalize(searched);
162
        if (isContains())
163
            return normalized.indexOf(this.normalizedFilterString) >= 0;
164
        else if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT)
165
            return normalized.equals(this.normalizedFilterString);
166
        else if (this.mode == Mode.LESS_THAN)
167
            return normalized.compareTo(this.normalizedFilterString) <= 0;
168
        else if (this.mode == Mode.GREATER_THAN)
169
            return normalized.compareTo(this.normalizedFilterString) >= 0;
170
        else
171
            throw new IllegalArgumentException("unknown mode " + this.mode);
172
    }
173
 
174
    private boolean isContainsOrEquals() {
175
        // only real Strings can be sorted (it makes no sense to sort 12/25/2010)
176
        return this.mode != Mode.LESS_THAN && this.mode != Mode.GREATER_THAN;
177
    }
178
 
179
    private boolean isContains() {
180
        return this.mode == Mode.CONTAINS || this.mode == Mode.CONTAINS_STRICT;
181
    }
182
 
183
    private boolean containsOrEquals(final String searched) {
184
        // don't normalize otherwise 2005-06-20 matches 2006 :
185
        // searched is first formatted to 20/06/2005 then normalized to 20062005
186
        if (isContains()) {
187
            return searched.indexOf(this.filterString) >= 0;
188
        } else {
189
            assert this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT : "Only call contains() if isContainsOrEquals()";
190
            return searched.equals(this.filterString);
191
        }
192
    }
193
 
194
    @SuppressWarnings("unchecked")
195
    private boolean test(Object search, final Object cell) {
196
        assert !(this.mode == Mode.CONTAINS || this.mode == Mode.CONTAINS_STRICT) : "Only call test() if not isContains()";
197
        if (search == null)
198
            return false;
199
 
200
        if (cell instanceof Comparable) {
201
            final Comparable c = (Comparable<?>) cell;
28 ilm 202
            if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT) {
203
                // allow to compare Timestamp & Date
204
                return c.compareTo(search) == 0;
205
            } else if (this.mode == Mode.LESS_THAN) {
17 ilm 206
                return c.compareTo(search) <= 0;
207
            } else {
208
                assert this.mode == Mode.GREATER_THAN;
209
                return c.compareTo(search) >= 0;
210
            }
211
        } else {
28 ilm 212
            if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT)
213
                return cell.equals(search);
214
            else
215
                return false;
17 ilm 216
        }
217
    }
218
 
219
    private FormatGroup getFormat(Object cell) {
220
        final Class<?> clazz = cell.getClass();
221
        if (!this.formats.containsKey(clazz)) {
222
            // cache the findings (eg sql.Date can be formatted like util.Date)
223
            this.formats.put(clazz, findFormat(clazz));
224
        }
225
        return this.formats.get(clazz);
226
    }
227
 
228
    // find if there's a format for cell
229
    // 1st tries its class, then goes up the hierarchy
230
    private FormatGroup findFormat(final Class<?> clazz) {
231
        Class<?> c = clazz;
232
        FormatGroup res = null;
233
        while (res == null && c != Object.class) {
234
            res = this.formats.get(c);
235
            c = c.getSuperclass();
236
        }
237
        return res;
238
    }
239
 
240
    @Override
241
    public boolean match(Object line) {
242
        return this.isEmpty() || matchWithFormats(line);
243
    }
244
 
245
    @Override
246
    public String toString() {
247
        return this.getClass().getSimpleName() + ":" + this.filterString;
248
    }
249
 
250
    @Override
251
    public boolean isEmpty() {
252
        return this.filterString == null || this.filterString.length() == 0;
253
    }
254
 
255
    public void setFormats(Map<Class<?>, FormatGroup> formats) {
256
        this.formats.clear();
257
        this.formats.putAll(formats);
258
    }
259
}