1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.value; |
7 | |
8 | import java.text.Collator; |
9 | import java.util.Locale; |
10 | |
11 | import org.h2.engine.SysProperties; |
12 | import org.h2.util.StringUtils; |
13 | |
14 | /** |
15 | * Instances of this class can compare strings. Case sensitive and case |
16 | * insensitive comparison is supported, and comparison using a collator. |
17 | */ |
18 | public class CompareMode { |
19 | |
20 | /** |
21 | * This constant means there is no collator set, and the default string |
22 | * comparison is to be used. |
23 | */ |
24 | public static final String OFF = "OFF"; |
25 | |
26 | /** |
27 | * This constant means the default collator should be used, even if ICU4J is |
28 | * in the classpath. |
29 | */ |
30 | public static final String DEFAULT = "DEFAULT_"; |
31 | |
32 | /** |
33 | * This constant means ICU4J should be used (this will fail if it is not in |
34 | * the classpath). |
35 | */ |
36 | public static final String ICU4J = "ICU4J_"; |
37 | |
38 | /** |
39 | * This constant means that the BINARY columns are sorted as if the bytes |
40 | * were signed. |
41 | */ |
42 | public static final String SIGNED = "SIGNED"; |
43 | |
44 | /** |
45 | * This constant means that the BINARY columns are sorted as if the bytes |
46 | * were unsigned. |
47 | */ |
48 | public static final String UNSIGNED = "UNSIGNED"; |
49 | |
50 | private static CompareMode lastUsed; |
51 | |
52 | private static final boolean CAN_USE_ICU4J; |
53 | |
54 | static { |
55 | boolean b = false; |
56 | try { |
57 | Class.forName("com.ibm.icu.text.Collator"); |
58 | b = true; |
59 | } catch (Exception e) { |
60 | // ignore |
61 | } |
62 | CAN_USE_ICU4J = b; |
63 | } |
64 | |
65 | private final String name; |
66 | private final int strength; |
67 | |
68 | /** |
69 | * If true, sort BINARY columns as if they contain unsigned bytes. |
70 | */ |
71 | private final boolean binaryUnsigned; |
72 | |
73 | protected CompareMode(String name, int strength, boolean binaryUnsigned) { |
74 | this.name = name; |
75 | this.strength = strength; |
76 | this.binaryUnsigned = binaryUnsigned; |
77 | } |
78 | |
79 | /** |
80 | * Create a new compare mode with the given collator and strength. If |
81 | * required, a new CompareMode is created, or if possible the last one is |
82 | * returned. A cache is used to speed up comparison when using a collator; |
83 | * CollationKey objects are cached. |
84 | * |
85 | * @param name the collation name or null |
86 | * @param strength the collation strength |
87 | * @return the compare mode |
88 | */ |
89 | public static synchronized CompareMode getInstance(String name, |
90 | int strength) { |
91 | return getInstance(name, strength, SysProperties.SORT_BINARY_UNSIGNED); |
92 | } |
93 | |
94 | /** |
95 | * Create a new compare mode with the given collator and strength. If |
96 | * required, a new CompareMode is created, or if possible the last one is |
97 | * returned. A cache is used to speed up comparison when using a collator; |
98 | * CollationKey objects are cached. |
99 | * |
100 | * @param name the collation name or null |
101 | * @param strength the collation strength |
102 | * @param binaryUnsigned whether to compare binaries as unsigned |
103 | * @return the compare mode |
104 | */ |
105 | public static synchronized CompareMode getInstance(String name, |
106 | int strength, boolean binaryUnsigned) { |
107 | if (lastUsed != null) { |
108 | if (StringUtils.equals(lastUsed.name, name) && |
109 | lastUsed.strength == strength && |
110 | lastUsed.binaryUnsigned == binaryUnsigned) { |
111 | return lastUsed; |
112 | } |
113 | } |
114 | if (name == null || name.equals(OFF)) { |
115 | lastUsed = new CompareMode(name, strength, binaryUnsigned); |
116 | } else { |
117 | boolean useICU4J; |
118 | if (name.startsWith(ICU4J)) { |
119 | useICU4J = true; |
120 | name = name.substring(ICU4J.length()); |
121 | } else if (name.startsWith(DEFAULT)) { |
122 | useICU4J = false; |
123 | name = name.substring(DEFAULT.length()); |
124 | } else { |
125 | useICU4J = CAN_USE_ICU4J; |
126 | } |
127 | if (useICU4J) { |
128 | lastUsed = new CompareModeIcu4J(name, strength, binaryUnsigned); |
129 | } else { |
130 | lastUsed = new CompareModeDefault(name, strength, binaryUnsigned); |
131 | } |
132 | } |
133 | return lastUsed; |
134 | } |
135 | |
136 | /** |
137 | * Compare two characters in a string. |
138 | * |
139 | * @param a the first string |
140 | * @param ai the character index in the first string |
141 | * @param b the second string |
142 | * @param bi the character index in the second string |
143 | * @param ignoreCase true if a case-insensitive comparison should be made |
144 | * @return true if the characters are equals |
145 | */ |
146 | public boolean equalsChars(String a, int ai, String b, int bi, |
147 | boolean ignoreCase) { |
148 | char ca = a.charAt(ai); |
149 | char cb = b.charAt(bi); |
150 | if (ignoreCase) { |
151 | ca = Character.toUpperCase(ca); |
152 | cb = Character.toUpperCase(cb); |
153 | } |
154 | return ca == cb; |
155 | } |
156 | |
157 | /** |
158 | * Compare two strings. |
159 | * |
160 | * @param a the first string |
161 | * @param b the second string |
162 | * @param ignoreCase true if a case-insensitive comparison should be made |
163 | * @return -1 if the first string is 'smaller', 1 if the second string is |
164 | * smaller, and 0 if they are equal |
165 | */ |
166 | public int compareString(String a, String b, boolean ignoreCase) { |
167 | if (ignoreCase) { |
168 | return a.compareToIgnoreCase(b); |
169 | } |
170 | return a.compareTo(b); |
171 | } |
172 | |
173 | /** |
174 | * Get the collation name. |
175 | * |
176 | * @param l the locale |
177 | * @return the name of the collation |
178 | */ |
179 | public static String getName(Locale l) { |
180 | Locale english = Locale.ENGLISH; |
181 | String name = l.getDisplayLanguage(english) + ' ' + |
182 | l.getDisplayCountry(english) + ' ' + l.getVariant(); |
183 | name = StringUtils.toUpperEnglish(name.trim().replace(' ', '_')); |
184 | return name; |
185 | } |
186 | |
187 | /** |
188 | * Compare name name of the locale with the given name. The case of the name |
189 | * is ignored. |
190 | * |
191 | * @param locale the locale |
192 | * @param name the name |
193 | * @return true if they match |
194 | */ |
195 | static boolean compareLocaleNames(Locale locale, String name) { |
196 | return name.equalsIgnoreCase(locale.toString()) || |
197 | name.equalsIgnoreCase(getName(locale)); |
198 | } |
199 | |
200 | /** |
201 | * Get the collator object for the given language name or language / country |
202 | * combination. |
203 | * |
204 | * @param name the language name |
205 | * @return the collator |
206 | */ |
207 | public static Collator getCollator(String name) { |
208 | Collator result = null; |
209 | if (name.startsWith(ICU4J)) { |
210 | name = name.substring(ICU4J.length()); |
211 | } else if (name.startsWith(DEFAULT)) { |
212 | name = name.substring(DEFAULT.length()); |
213 | } |
214 | if (name.length() == 2) { |
215 | Locale locale = new Locale(StringUtils.toLowerEnglish(name), ""); |
216 | if (compareLocaleNames(locale, name)) { |
217 | result = Collator.getInstance(locale); |
218 | } |
219 | } else if (name.length() == 5) { |
220 | // LL_CC (language_country) |
221 | int idx = name.indexOf('_'); |
222 | if (idx >= 0) { |
223 | String language = StringUtils.toLowerEnglish(name.substring(0, idx)); |
224 | String country = name.substring(idx + 1); |
225 | Locale locale = new Locale(language, country); |
226 | if (compareLocaleNames(locale, name)) { |
227 | result = Collator.getInstance(locale); |
228 | } |
229 | } |
230 | } |
231 | if (result == null) { |
232 | for (Locale locale : Collator.getAvailableLocales()) { |
233 | if (compareLocaleNames(locale, name)) { |
234 | result = Collator.getInstance(locale); |
235 | break; |
236 | } |
237 | } |
238 | } |
239 | return result; |
240 | } |
241 | |
242 | public String getName() { |
243 | return name == null ? OFF : name; |
244 | } |
245 | |
246 | public int getStrength() { |
247 | return strength; |
248 | } |
249 | |
250 | public boolean isBinaryUnsigned() { |
251 | return binaryUnsigned; |
252 | } |
253 | |
254 | @Override |
255 | public boolean equals(Object obj) { |
256 | if (obj == this) { |
257 | return true; |
258 | } else if (!(obj instanceof CompareMode)) { |
259 | return false; |
260 | } |
261 | CompareMode o = (CompareMode) obj; |
262 | if (!getName().equals(o.getName())) { |
263 | return false; |
264 | } |
265 | if (strength != o.strength) { |
266 | return false; |
267 | } |
268 | if (binaryUnsigned != o.binaryUnsigned) { |
269 | return false; |
270 | } |
271 | return true; |
272 | } |
273 | |
274 | @Override |
275 | public int hashCode() { |
276 | return getName().hashCode() ^ strength ^ (binaryUnsigned ? -1 : 0); |
277 | } |
278 | |
279 | } |